W3docs

Java Maven Dependencies

Declare and manage Java dependencies in Maven with dependency, scope, and transitive resolution.

Java Maven Dependencies

Real Java projects rarely stand alone. They pull in logging frameworks, HTTP clients, JSON parsers, and test libraries written by other people. Maven's job is to fetch those libraries, fetch their libraries in turn, and assemble one consistent classpath without you tracking a single JAR by hand. Understanding how it does that is the difference between a build that just works and an afternoon lost to a NoSuchMethodError.

Coordinates: How a Dependency Is Named

Every artifact in the Maven world is identified by a set of coordinates. The three you always supply are the groupId (who publishes it, usually a reversed domain), the artifactId (the project's name), and the version. Together they point at exactly one JAR in a repository.

You declare a dependency inside the <dependencies> block of your pom.xml:

<dependencies>
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
  </dependency>
</dependencies>

The shorthand groupId:artifactId:version is called a GAV string, and you will see it everywhere: in error messages, in the dependency tree, and on the central repository's web pages. A fourth coordinate, the type (jar by default), and a fifth, the classifier (for variants like sources or javadoc), round out the full address.

Scopes: When a Dependency Is Visible

Not every dependency belongs on every classpath. A test framework should not ship in your production JAR, and a servlet API provided by the application server should not be bundled twice. Maven controls this with the <scope> element.

ScopeCompileTestRuntimePackagedTypical use
compile (default)YesYesYesYesCore libraries you call directly
providedYesYesNoNoAPIs the container supplies (servlet, JDBC driver)
runtimeNoYesYesYesImplementations needed only at run time
testNoYesNoNoJUnit, Mockito, assertion libraries
systemYesYesNoNoLocal JARs by absolute path (avoid)

A test-scoped dependency is the most common non-default. JUnit never leaks into your shipped artifact:

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.11.0</version>
  <scope>test</scope>
</dependency>

Transitive Dependencies

When you depend on a library, you also depend on everything it depends on. Maven reads each artifact's published pom.xml, follows those declarations recursively, and adds the whole graph to your classpath automatically. These indirect entries are transitive dependencies.

This is why a single <dependency> line for a web framework can drag in dozens of JARs you never named. Scope still applies during this walk: a test-scoped dependency does not pull its transitive dependencies into your compile classpath, and provided dependencies are not propagated transitively at all.

You can see the full graph with the dependency plugin:

$ mvn dependency:tree
[INFO] com.example:app:jar:1.0
[INFO] +- org.web:server:jar:2.4:compile
[INFO] |  +- org.log:log:jar:1.2:compile
[INFO] |  \- org.json:json:jar:1.7:compile - omitted for conflict with 1.9
[INFO] \- org.json:json:jar:1.9:compile

Version Conflict Mediation

A graph that deep almost always wants the same artifact at two different versions. Maven cannot put both on one classpath, so it picks one using nearest-wins mediation: the version declared at the shallowest depth from your project root prevails, and the others are omitted for conflict.

In the tree above, your project asks for org.json:json:1.9 directly (depth 1), while org.web:server asks for 1.7 transitively (depth 2). The depth-1 declaration wins. If two candidates sit at the same depth, the one declared first in the pom.xml wins.

When the automatic choice is wrong, you take control explicitly. A direct dependency always wins, or you can pin versions across the whole project with <dependencyManagement>:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
      <version>1.9</version>
    </dependency>
  </dependencies>
</dependencyManagement>

To cut an unwanted transitive branch entirely, use <exclusions>:

<dependency>
  <groupId>org.web</groupId>
  <artifactId>server</artifactId>
  <version>2.4</version>
  <exclusions>
    <exclusion>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
    </exclusion>
  </exclusions>
</dependency>

A Complete Example

Maven itself is not on this code runner, so the program below models its resolver in plain Java. It publishes a few artifacts into a tiny in-memory repository, then performs the same breadth-first walk and nearest-wins mediation Maven uses to flatten a dependency graph into one classpath. Watch how the deeper org.json:json:1.7 loses to the shallower 1.9.

java— editable, runs on the server

What to take from the run:

  • The resolved classpath lists each artifact exactly once, mirroring how Maven flattens a graph into a single set of JARs with no duplicate group:artifact entries.
  • org.json:json appears at version 1.9, not 1.7, because nearest-wins mediation keeps the candidate found at the shallower depth (depth 1 beats depth 2).
  • The depth column makes "nearest" concrete: app:app is depth 0, its direct dependencies are depth 1, and transitively pulled org.log:log is depth 2.
  • "Tree edges visited: 4" counts the declared dependency relationships, while "Distinct artifacts: 4" shows the graph collapsed to four unique coordinates after mediation.
  • A coordinate is skipped the moment it is seen again (depthOf.containsKey(ga)), which is exactly why the deeper 1.7 is "omitted for conflict" rather than added a second time.

Practice

Practice

In Maven, two versions of the same artifact appear in the dependency graph: version 1.9 declared directly in your pom (depth 1) and version 1.7 pulled in transitively (depth 2). Which version ends up on the classpath?