Maven dependency resolution
last modified November 8, 2026
In this article we show how Maven resolves dependencies: where it gets them from, how it handles transitive dependencies, and how it picks one version when several are requested. The examples use Maven 3.10 and Java 25.
Dependency resolution is the process in which Maven turns the dependencies declared in the POM into a list of JAR files on the classpath. Maven reads the POM of every declared dependency, adds the dependencies listed there (transitive dependencies), and repeats this until the whole graph is known. Then it removes duplicates and conflicting versions, so that each artifact appears on the classpath only once.
Transitive dependencies
Our first project declares a single dependency, Apache Commons Text.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>basic</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.15.0</version>
</dependency>
</dependencies>
</project>
The POM declares only commons-text.
package com.example;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringSubstitutor;
import java.util.Map;
public class App {
public static void main(String[] args) {
var values = Map.of("name", "Maven", "version", "3.10");
var sub = new StringSubstitutor(values);
String text = sub.replace("${name} ${version} resolves dependencies");
System.out.println(text);
System.out.println(StringUtils.capitalize(StringUtils.reverse(text)));
}
}
The application uses StringSubstitutor from Commons Text and
StringUtils from Commons Lang. We did not declare Commons Lang,
but it is a dependency of Commons Text, so Maven puts it on the classpath as
well. The dependency:tree goal shows the resolved graph.
$ mvn dependency:tree [INFO] com.example:basic:jar:1.0.0 [INFO] \- org.apache.commons:commons-text:jar:1.15.0:compile [INFO] \- org.apache.commons:commons-lang3:jar:3.20.0:compile ... $ mvn -q compile exec:java -Dexec.mainClass=com.example.App Maven 3.10 resolves dependencies Seicnedneped sevloser 01.3 nevaM
The local repository
Maven downloads artifacts from remote repositories, by default from Maven
Central, and stores them in the local repository. Later builds take them from
there without network access. The help:evaluate goal prints the
location of the local repository; the artifacts are stored in directories
derived from their coordinates.
Besides the JAR and the POM, each version directory contains checksum files
and the _remote.repositories file, which records where the
artifact came from. With the -o (offline) option, Maven uses
only the local repository.
$ mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout /home/user/.m2/repository $ ls ~/.m2/repository/org/apache/commons/commons-text/1.15.0 _remote.repositories commons-text-1.15.0.jar commons-text-1.15.0.jar.md5 commons-text-1.15.0.jar.sha1 commons-text-1.15.0.pom commons-text-1.15.0.pom.md5 commons-text-1.15.0.pom.sha1
Nearest definition wins
When the graph contains several versions of the same artifact, Maven chooses one by dependency mediation. The first rule is that the nearest definition wins: the version closest to our project in the tree is used. A direct dependency is always the nearest.
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.15.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
We declare an older version of Commons Lang directly. Commons Text 1.15.0
asks for 3.20.0, but our 3.12.0 is nearer, so it wins. The
-Dverbose option shows the omitted versions and the reason.
Note that Maven does not pick the highest version. Here it downgrades a
library that Commons Text was built and tested against; if Commons Text calls
a method added after 3.12, the application fails at runtime with a
NoSuchMethodError.
$ mvn dependency:tree -Dverbose [INFO] com.example:nearest:jar:1.0.0 [INFO] +- org.apache.commons:commons-text:jar:1.15.0:compile [INFO] | \- (org.apache.commons:commons-lang3:jar:3.20.0:compile - omitted for conflict with 3.12.0) [INFO] \- org.apache.commons:commons-lang3:jar:3.12.0:compile ...
First declaration wins
If two versions are at the same depth, the first one declared in the POM wins. In the following project, both Commons Compress and Commons Text depend on Commons Lang.
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.28.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.15.0</version>
</dependency>
</dependencies>
Commons Compress is declared first, so its version 3.18.0 is chosen, even though Commons Text asks for the newer 3.20.0. Swapping the two dependencies in the POM changes the resolved version.
$ mvn dependency:tree -Dverbose [INFO] com.example:firstwins:jar:1.0.0 [INFO] +- org.apache.commons:commons-compress:jar:1.28.0:compile [INFO] | +- commons-codec:commons-codec:jar:1.19.0:compile [INFO] | +- commons-io:commons-io:jar:2.20.0:compile [INFO] | \- org.apache.commons:commons-lang3:jar:3.18.0:compile [INFO] \- org.apache.commons:commons-text:jar:1.15.0:compile [INFO] \- (org.apache.commons:commons-lang3:jar:3.20.0:compile - omitted for conflict with 3.18.0) ...
Managing versions
Relying on depth and declaration order is fragile. The
dependencyManagement section fixes the version of an artifact
wherever it appears in the graph, including transitive dependencies.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.21.0</version>
</dependency>
</dependencies>
</dependencyManagement>
The managed version does not add a dependency by itself; it applies only when
the artifact is already part of the graph. A dependency declared in the
dependencies section can then omit its version.
The verbose tree marks the overridden versions with version managed
from. Both paths now lead to Commons Lang 3.21.0. In multi-module
projects, the dependencyManagement section of the parent POM, or
an imported BOM, keeps the versions consistent across all modules.
$ mvn dependency:tree -Dverbose [INFO] com.example:managed:jar:1.0.0 [INFO] +- org.apache.commons:commons-compress:jar:1.28.0:compile [INFO] | +- commons-codec:commons-codec:jar:1.19.0:compile [INFO] | +- commons-io:commons-io:jar:2.20.0:compile [INFO] | \- org.apache.commons:commons-lang3:jar:3.21.0:compile (version managed from 3.18.0) [INFO] \- org.apache.commons:commons-text:jar:1.15.0:compile [INFO] \- (org.apache.commons:commons-lang3:jar:3.21.0:compile - version managed from 3.20.0; omitted for duplicate) ...
Excluding transitive dependencies
An exclusion removes a transitive dependency from one branch of the graph. This is used to drop a library we do not want, or to replace it with a compatible one. The next project uses Commons Configuration to read a YAML file.
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.15.1</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>2.0.20</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.20</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Commons Configuration logs through Commons Logging. We exclude it and add the
jcl-over-slf4j bridge, which provides the Commons Logging API and
forwards the messages to SLF4J; slf4j-simple is the SLF4J
backend. An exclusion takes only the group ID and the artifact ID.
The exclusion must be replaced by something that provides the same classes.
Without the bridge, the application fails with a
NoClassDefFoundError for org/apache/commons/logging/Log.
The tree no longer contains commons-logging. The
snakeyaml dependency is explained in the next section.
$ mvn dependency:tree [INFO] com.example:config:jar:1.0.0 [INFO] +- org.apache.commons:commons-configuration2:jar:2.15.1:compile [INFO] | +- org.apache.commons:commons-lang3:jar:3.20.0:compile [INFO] | +- org.apache.commons:commons-text:jar:1.15.0:compile [INFO] | \- commons-io:commons-io:jar:2.22.0:compile [INFO] +- org.yaml:snakeyaml:jar:2.6:compile [INFO] +- org.slf4j:jcl-over-slf4j:jar:2.0.20:compile [INFO] | \- org.slf4j:slf4j-api:jar:2.0.20:compile [INFO] \- org.slf4j:slf4j-simple:jar:2.0.20:runtime ...
Optional dependencies
A library can mark a dependency as optional with
<optional>true</optional>. An optional dependency is
used when the library itself is built, but it is not passed on to projects
that depend on the library. It is typically used for features that only some
users need. Commons Configuration declares its YAML parser, SnakeYAML, as
optional, so users who do not read YAML files do not get it.
server: host: localhost port: 8080 database: url: jdbc:h2:mem:test pool: 5
The configuration file read by the application.
package com.example;
import org.apache.commons.configuration2.YAMLConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.io.FileHandler;
public class App {
public static void main(String[] args) throws ConfigurationException {
var config = new YAMLConfiguration();
new FileHandler(config).load("app.yaml");
System.out.println("Server: " + config.getString("server.host")
+ ":" + config.getInt("server.port"));
System.out.println("Database: " + config.getString("database.url"));
System.out.println("Pool size: " + config.getInt("database.pool"));
}
}
The application loads the YAML file and reads a few values with keys in dotted notation.
We run the application twice: first from a copy of the project without the SnakeYAML dependency, which fails because the YAML parser classes are missing, and then from the project that declares SnakeYAML explicitly.
$ cd noyaml $ mvn -q compile exec:java -Dexec.mainClass=com.example.App [ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.6.4:java (default-cli) on project noyaml: An exception occurred while executing the Java class. org/yaml/snakeyaml/constructor/BaseConstructor: org.yaml.snakeyaml.constructor.BaseConstructor -> [Help 1] ... $ cd ../config $ mvn -q compile exec:java -Dexec.mainClass=com.example.App Server: localhost:8080 Database: jdbc:h2:mem:test Pool size: 5
Analyzing dependencies
The dependency:analyze goal compares the declared dependencies
with the classes the compiled code actually uses. Our first project uses
StringUtils from Commons Lang, which it gets only transitively.
Using an undeclared dependency is risky: if Commons Text stops depending on Commons Lang, our code no longer compiles. The fix is to declare every library the code uses directly. The goal also reports declared dependencies that are not used.
$ mvn dependency:analyze [WARNING] Used undeclared dependencies found: [WARNING] org.apache.commons:commons-lang3:jar:3.20.0:compile [INFO] BUILD SUCCESS ...
Maven 3.10 changes
Maven 3.10 uses version 2 of the Maven Resolver, the library that does the
dependency resolution. Two of its changes affect existing builds. The
classpath is now ordered level by level (breadth-first): all direct
dependencies come first, then their dependencies, and so on. Before, the
classpath followed the tree depth-first. This only matters when two JAR files
contain the same class; the old order can be restored with
-Daether.system.dependencyVisitor=preOrder.
Maven 3.10 also validates artifact coordinates and POMs more strictly. Many
problems that produced warnings in Maven 3.9 now fail the build. The
-Dmaven.resolver.validation=off option disables the new checks
temporarily, but the coordinates should be fixed.
Best practices
- Declare every library the code uses directly; check with
mvn dependency:analyze. - Use
dependencyManagementor a BOM to fix versions instead of relying on depth and declaration order. - Check version conflicts with
mvn dependency:tree -Dverboseafter adding or upgrading a dependency. - When excluding a dependency, make sure the classes it provided are not needed or are supplied by a replacement.
- Keep dependencies up to date;
mvn versions:display-dependency-updateslists newer versions.
Source
Introduction to the dependency mechanism
In this article we have shown how Maven resolves dependencies, how it mediates version conflicts, and how to control the result with dependency management, exclusions, and optional dependencies.
Author
List all Java tutorials.