ZetCode

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.

pom.xml
<?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.

src/main/java/com/example/DependencyApp.java
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.

pom.xml (dependency scopes)
<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.

pom.xml (version 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.

pom.xml (dependency management)
<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.

pom.xml (dependency exclusions)
<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.

Example: Running without explicit plugin configuration
<!-- 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:

pom.xml (explicit exec plugin configuration)
<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.

pom.xml (custom repositories)
<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.

pom.xml (optional dependencies)
<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.

pom.xml (system dependencies)
<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.

pom.xml (import scope)
<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:

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.