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.
| Scope | Compile | Test | Runtime | Packaged | Typical use |
|---|---|---|---|---|---|
compile (default) | Yes | Yes | Yes | Yes | Core libraries you call directly |
provided | Yes | Yes | No | No | APIs the container supplies (servlet, JDBC driver) |
runtime | No | Yes | Yes | Yes | Implementations needed only at run time |
test | No | Yes | No | No | JUnit, Mockito, assertion libraries |
system | Yes | Yes | No | No | Local 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:compileVersion 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.
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:jsonappears at version1.9, not1.7, because nearest-wins mediation keeps the candidate found at the shallower depth (depth 1 beats depth 2).- The
depthcolumn makes "nearest" concrete:app:appis depth 0, its direct dependencies are depth 1, and transitively pulledorg.log:logis 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 deeper1.7is "omitted for conflict" rather than added a second time.
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?