Maven Dependency Resolution
last modified June 10, 2025
In this article we show how Maven resolves dependencies and manages the dependency tree. Maven's dependency resolution mechanism handles direct dependencies, transitive dependencies, and version conflicts automatically.
Maven dependency resolution is the process by which Maven determines which artifacts (JARs, WARs, etc.) are needed for compilation, testing, and runtime. It analyzes your project's dependencies and their transitive dependencies to create a complete classpath.
Transitive dependencies are dependencies of your declared dependencies. Maven automatically resolves these, ensuring that all required libraries are available without you needing to declare them explicitly. This feature simplifies dependency management and reduces the need for manual configuration.
Basic Dependency Declaration
Let's start with a basic example that demonstrates how to declare dependencies and understand how Maven resolves them.
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>dependency-example</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> </project>
This basic configuration declares three dependencies: Apache Commons Lang3, Jackson Databind, and JUnit. Maven will automatically resolve and download these dependencies along with their transitive dependencies.
Understanding Transitive Dependencies
When you declare a dependency, Maven automatically includes its transitive dependencies. For example, jackson-databind depends on jackson-core and jackson-annotations, which Maven will download automatically.
package com.example; import org.apache.commons.lang3.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.JsonNode; import java.util.HashMap; import java.util.Map; public class DependencyApp { public static void main(String[] args) throws Exception { System.out.println("Maven Dependency Resolution Example"); // Using Apache Commons Lang3 String text = " Hello Maven Dependencies "; String trimmed = StringUtils.strip(text); System.out.println("Trimmed text: '" + trimmed + "'"); // Using Jackson for JSON processing ObjectMapper mapper = new ObjectMapper(); Map<String, Object> data = new HashMap<>(); data.put("project", "Maven Dependency Example"); data.put("dependencies", new String[]{"commons-lang3", "jackson"}); data.put("resolved", true); String json = mapper.writeValueAsString(data); System.out.println("JSON output: " + json); // Parse back JsonNode node = mapper.readTree(json); System.out.println("Project name: " + node.get("project").asText()); } }
This Java application uses the declared dependencies to demonstrate how Maven resolves and manages them. It uses Apache Commons Lang3 for string manipulation and Jackson for JSON processing. When you run this application, Maven will automatically download the required dependencies and their transitive dependencies.
Running Without Exec Plugin Declaration
Here's an interesting Maven feature: you can run a main class using the exec plugin even if it's not declared in your pom.xml. Maven will automatically download and use the plugin when needed.
$ mvn compile $ mvn exec:java -Dexec.mainClass="com.example.DependencyApp"
Maven will automatically download the exec plugin and its dependencies, then execute your main class. This demonstrates Maven's plugin resolution mechanism working alongside dependency resolution.
[INFO] --- exec-maven-plugin:3.1.0:java (default-cli) @ dependency-example --- Maven Dependency Resolution Example Trimmed text: 'Hello Maven Dependencies' JSON output: {"project":"Maven Dependency Example","dependencies":["commons-lang3","jackson"],"resolved":true} Project name: Maven Dependency Example
Dependency Tree Analysis
Maven provides tools to analyze and understand the dependency tree. The dependency tree shows all resolved dependencies including transitive ones.
$ mvn dependency:tree
This command displays the complete dependency tree for your project:
[INFO] com.example:dependency-example:jar:1.0.0 [INFO] +- org.apache.commons:commons-lang3:jar:3.14.0:compile [INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.17.1:compile [INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.17.1:compile [INFO] | \- com.fasterxml.jackson.core:jackson-core:jar:2.17.1:compile [INFO] \- junit:junit:jar:4.13.2:test [INFO] \- org.hamcrest:hamcrest-core:jar:1.3:test
The tree shows that jackson-databind brings in two transitive dependencies: jackson-annotations and jackson-core. JUnit brings in hamcrest-core as a transitive dependency.
Analyzing Specific Dependencies
You can also analyze dependencies for specific artifacts:
$ mvn dependency:tree -Dincludes=com.fasterxml.jackson.core
This shows only Jackson-related dependencies in the tree, helping you understand specific dependency chains.
Dependency Scopes and Resolution
Maven supports different dependency scopes that affect when dependencies are available and how they're resolved.
<dependencies> <!-- Compile scope (default) - available everywhere --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> <scope>compile</scope> </dependency> <!-- Runtime scope - available at runtime and testing --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> <scope>runtime</scope> </dependency> <!-- Test scope - only available during testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> <!-- Provided scope - available at compile time but not packaged --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>
Maven supports several dependency scopes that determine how dependencies are resolved and included in the build. The most common scopes are: compile, runtime, test, and provided.
<scope>compile</scope>
Default scope. Dependencies are available in all classpaths and included in the final artifact. Used for dependencies needed at compile time and runtime.
<scope>runtime</scope>
Dependencies not needed for compilation but required at runtime. Typically used for database drivers or logging implementations.
<scope>test</scope>
Dependencies only available during test compilation and execution. Not included in the final artifact. Used for testing frameworks and utilities.
<scope>provided</scope>
Dependencies available at compile time but not packaged. Expected to be provided by the runtime environment (like servlet APIs in web containers).
Version Conflict Resolution
When multiple versions of the same dependency are found in the dependency tree, Maven uses specific rules to resolve conflicts.
<dependencies> <!-- Direct dependency on commons-lang3 3.14.0 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency> <!-- This dependency might require commons-lang3 3.12.0 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-configuration2</artifactId> <version>2.10.1</version> </dependency> </dependencies>
Maven resolves version conflicts using the " nearest wins" strategy: the version closest to your project in the dependency tree is selected. Direct dependencies take precedence over transitive dependencies.
Analyzing Version Conflicts
Use the dependency plugin to identify version conflicts:
$ mvn dependency:tree -Dverbose
This shows omitted dependencies and explains why certain versions were chosen:
[INFO] +- org.apache.commons:commons-lang3:jar:3.14.0:compile [INFO] +- org.apache.commons:commons-configuration2:jar:2.10.1:compile [INFO] | +- (org.apache.commons:commons-lang3:jar:3.12.0:compile - omitted for conflict with 3.14.0)
Explicit Version Management
Use dependency management to explicitly control versions across your project. This is especially useful in multi-module projects where you want to ensure consistent versions across all modules.
The dependencyManagement
tag allows you to define versions for
dependencies in one place, which can then be referenced in multiple modules
without needing to specify the version each time.
<dependencyManagement> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.1</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Version managed by dependencyManagement --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies>
In our case, the dependencyManagement
section defines the versions
for commons-lang3
and jackson-databind
. When we
declare these dependencies in the dependencies
section without
specifying the version, Maven uses the versions defined in
dependencyManagement
.
Excluding Transitive Dependencies
Sometimes you need to exclude specific transitive dependencies to avoid conflicts or reduce the dependency footprint.
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>6.1.8</version> <exclusions> <!-- Exclude commons-logging to use SLF4J instead --> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- Provide SLF4J implementation instead --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>2.0.13</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.6</version> </dependency> </dependencies>
This configuration excludes commons-logging from Spring and replaces it with SLF4J bridging, providing better logging control and avoiding conflicts.
<exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions>
Exclusions prevent specific transitive dependencies from being included. Only groupId and artifactId are needed; version is not specified in exclusions.
Plugin Resolution and Automatic Downloads
Maven automatically resolves and downloads plugins when they're used, even if not explicitly declared. This includes commonly used plugins like the exec plugin for running Java applications.
<!-- No exec plugin declared in pom.xml --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.12.1</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build>
Even without declaring the exec plugin, you can still use it:
$ mvn exec:java -Dexec.mainClass="com.example.DependencyApp"
Maven will output something like:
[INFO] Downloading from central: https://repo1.maven.org/maven2/org/codehaus/mojo/exec-maven-plugin/3.1.0/exec-maven-plugin-3.1.0.pom [INFO] Downloaded from central: https://repo1.maven.org/maven2/org/codehaus/mojo/exec-maven-plugin/3.1.0/exec-maven-plugin-3.1.0.pom (11 kB) [INFO] Downloading from central: https://repo1.maven.org/maven2/org/codehaus/mojo/exec-maven-plugin/3.1.0/exec-maven-plugin-3.1.0.jar [INFO] Downloaded from central: https://repo1.maven.org/maven2/org/codehaus/mojo/exec-maven-plugin/3.1.0/exec-maven-plugin-3.1.0.jar (61 kB) [INFO] --- exec-maven-plugin:3.1.0:java (default-cli) @ dependency-example ---
This demonstrates Maven's plugin resolution mechanism working automatically to provide functionality when needed.
Explicit Plugin Configuration
For production projects, it's better to explicitly configure plugins:
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.1.0</version> <configuration> <mainClass>com.example.DependencyApp</mainClass> <cleanupDaemonThreads>false</cleanupDaemonThreads> </configuration> <executions> <execution> <id>run-app</id> <goals> <goal>java</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
This configuration explicitly declares the exec-maven-plugin and specifies the main class to run. It ensures that the plugin is always available and configured correctly, avoiding potential issues with automatic resolution.
The executions
tag allows you to define specific goals to run the
plugin during the build lifecycle. In this case, we define an execution with the
ID run-app
that runs the java
goal of the exec plugin.
This makes it easier to run your application with a simple command:
$ mvn exec:java -Dexec.mainClass="com.example.DependencyApp"
This command will execute the main class specified in the plugin configuration, ensuring that the correct class is run without needing to specify it each time.
With explicit configuration, you can simply run:
$ mvn exec:java
The plugin will use the configured main class automatically.
Dependency Resolution Troubleshooting
Maven provides several tools for troubleshooting dependency resolution issues.
Effective POM Analysis
View the effective POM to see how Maven resolves your configuration:
$ mvn help:effective-pom
This shows the complete POM after all inheritance, profiles, and properties are resolved.
Dependency Analysis
Analyze your dependencies for unused or undeclared dependencies:
$ mvn dependency:analyze
This command identifies:
[WARNING] Used undeclared dependencies found: [WARNING] com.fasterxml.jackson.core:jackson-core:jar:2.17.1:compile [WARNING] Unused declared dependencies found: [WARNING] org.apache.commons:commons-lang3:jar:3.14.0:compile
The analysis helps optimize your dependency declarations by identifying issues like unused direct dependencies or missing declarations for directly used code.
Resolving Dependency Sources
Download source JARs for better IDE integration and debugging:
$ mvn dependency:sources
This downloads source attachments for all dependencies, making it easier to debug and understand third-party code in your IDE.
Repository Configuration
Maven resolves dependencies from repositories. You can configure custom repositories for specific dependencies.
<repositories> <repository> <id>central</id> <name>Maven Central Repository</name> <url>https://repo1.maven.org/maven2</url> <layout>default</layout> </repository> <repository> <id>spring-releases</id> <name>Spring Release Repository</name> <url>https://repo.spring.io/release</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>central</id> <name>Maven Plugin Repository</name> <url>https://repo1.maven.org/maven2</url> <layout>default</layout> </pluginRepository> </pluginRepositories>
Repository configuration allows Maven to resolve dependencies from multiple sources, including private repositories or alternative public repositories.
Advanced Dependency Features
Maven supports advanced dependency features for complex scenarios.
Optional Dependencies
Optional dependencies are not required for the main functionality of your
project but can be included if needed. They are declared with the
optional
scope, which prevents them from being transitively
included in projects that depend on your artifact.
<dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-email</artifactId> <version>1.5</version> <optional>true</optional> </dependency> </dependencies>
Optional dependencies are not transitively included when other projects depend on your artifact. Consumers must explicitly declare them if needed.
System Dependencies
System dependencies are used to include a JAR file that is not available in any remote repository. They are typically used for local libraries or system JARs that are not managed by Maven.
<dependencies> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc8</artifactId> <version>19.3.0.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/ojdbc8.jar</systemPath> </dependency> </dependencies>
System dependencies require specifying a local JAR file path, making builds less portable. They are similar to provided scope but are discouraged in favor of installing dependencies to a local or corporate repository.
<scope>system</scope> <systemPath>${project.basedir}/lib/ojdbc8.jar</systemPath>
System scope dependencies rely on a specific file path, which can break builds if the file is unavailable or paths differ across environments.
Bill of Materials (BOM) and Import Scope
The import
scope is used in the dependencyManagement
section to import dependency configurations from a Bill of Materials (BOM) POM,
ensuring consistent versions across dependencies.
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.2.5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
The import scope simplifies version management by inheriting dependency versions from a BOM, such as Spring Boot's, ensuring compatibility.
<scope>import</scope> <type>pom</type>
Used exclusively in dependencyManagement
to import versioned
dependency configurations from another POM.
Best Practices for Dependency Resolution
Here are some best practices to optimize Maven dependency resolution:
- Use dependencyManagement for version control - Centralize version definitions to ensure consistency across multi-module projects.
- Minimize transitive dependencies - Use exclusions to remove unnecessary or conflicting dependencies.
-
Analyze dependencies regularly - Run
mvn dependency:tree
andmvn dependency:analyze
to identify issues. - Avoid system scope - Install dependencies to a repository instead of using local file paths.
- Use BOMs for complex projects - Import BOMs like Spring Boot or JUnit to simplify dependency management.
-
Keep dependencies up-to-date - Use
mvn versions:display-dependency-updates
to check for newer versions. - Explicitly declare plugins - Configure plugins like exec-maven-plugin in the POM for predictable builds.
Source
Maven Dependency Mechanism - reference
In this article, we have shown how Maven resolves dependencies, handles transitive dependencies, resolves version conflicts, and provides tools for troubleshooting and optimization.
Author
List all Java tutorials.