Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

Multi-module Maven projects can have complex dependency graphs. These can have unusual results, the more the modules import from each other.

In this tutorial, we'll see how to resolve version collision of artifacts in Maven.

We'll start with a multi-module project where we've deliberately used different versions of the same artifact. Then, we'll see how to prevent getting the wrong version of an artifact with either exclusion or dependency management.

Finally, we'll try using the maven-enforcer-plugin to make things easier to control, by banning the use of transitive dependencies.

2. Version Collision of Artifacts

Each dependency that we include in our project might link to other artifacts. Maven can automatically bring in these artifacts, also called transitive dependencies. Version collision happens when multiple dependencies link to the same artifact, but use different versions.

As a result, there may be errors in our applications both in the compilation phase and also at runtime.

2.1. Project Structure

Let's define a multi-module project structure to experiment with. Our project consists of a version-collision parent and three children modules:

version-collision
    project-a
    project-b
    project-collision

The pom.xml for project-a and project-b are almost identical. The only difference is the version of the com.google.guava artifact that they depend on. In particular, project-a uses version 22.0:

<dependencies>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>22.0</version>
    </dependency>
</dependencies>

But, project-b uses the newer version, 29.0-jre:

<dependencies>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>29.0-jre</version>
    </dependency>
</dependencies>

The third module, project-collision, depends on the other two:

<dependencies>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-a</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-b</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

So, which version of guava will be available to project-collision?

2.2. Using Features from Specific Dependency Version

We can find out which dependency is used by creating a simple test in the project-collision module that uses the Futures.immediateVoidFuture method from guava:

@Test
public void whenVersionCollisionDoesNotExist_thenShouldCompile() {
    assertThat(Futures.immediateVoidFuture(), notNullValue());
}

This method is only available from the 29.0-jre version. We've inherited this from one of the other modules, but we can only compile our code if we got the transitive dependency from project-b.

2.3. Compilation Error Caused by Version Collision

Depending on the order of dependencies in the project-collision module, in certain combinations Maven returns a compilation error:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:testCompile (default-testCompile) on project project-collision: Compilation failure
[ERROR] /tutorials/maven-all/version-collision/project-collision/src/test/java/com/baeldung/version/collision/VersionCollisionUnitTest.java:[12,27] cannot find symbol
[ERROR]   symbol:   method immediateVoidFuture()
[ERROR]   location: class com.google.common.util.concurrent.Futures

That's the result of the version collision of the com.google.guava artifact. By default, for dependencies at the same level in a dependency tree, Maven chooses the first library it finds. In our case, both com.google.guava dependencies are at the same height and the older version is chosen.

2.4. Using maven-dependency-plugin

The maven-dependency-plugin is a very helpful tool to present all dependencies and their versions:

% mvn dependency:tree -Dverbose

[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ project-collision ---
[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] +- com.baeldung:project-a:jar:0.0.1-SNAPSHOT:compile
[INFO] |  \- com.google.guava:guava:jar:22.0:compile
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- (com.google.guava:guava:jar:29.0-jre:compile - omitted for conflict with 22.0)

The -Dverbose flag displays conflicting artifacts. In fact, we have a com.google.guava dependency in two versions: 22.0 and 29.0-jre. The latter is the one we would like to use in the project-collision module.

3. Excluding a Transitive Dependency From an Artifact

One way to resolve a version collision is by removing a conflicting transitive dependency from specific artifacts. In our example, we don't want to have the com.google.guava library transitively added from the project-a artifact.

Therefore, we can exclude it in the project-collision pom:

<dependencies>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-a</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <exclusions>
            <exclusion>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.baeldung</groupId>
        <artifactId>project-b</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

Now, when we run the dependency:tree command, we can see that it's not there anymore:

% mvn dependency:tree -Dverbose

[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ project-collision ---
[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- com.google.guava:guava:jar:29.0-jre:compile

As a result, the compilation phase ends without an error and we can use the classes and methods from version 29.0-jre.

4. Using the dependencyManagement Section

Maven's dependencyManagement section is a mechanism for centralizing dependency information. One of its most useful features is to control versions of artifacts used as transitive dependencies.

With that in mind, let's create a dependencyManagement configuration in our parent pom:

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
         <version>29.0-jre</version>
      </dependency>
   </dependencies>
</dependencyManagement>

As a result, Maven will make sure to use version 29.0-jre of com.google.guava artifact in all child modules:

% mvn dependency:tree -Dverbose

[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ project-collision ---
[INFO] com.baeldung:project-collision:jar:0.0.1-SNAPSHOT
[INFO] +- com.baeldung:project-a:jar:0.0.1-SNAPSHOT:compile
[INFO] |  \- com.google.guava:guava:jar:29.0-jre:compile (version managed from 22.0)
[INFO] \- com.baeldung:project-b:jar:0.0.1-SNAPSHOT:compile
[INFO]    \- (com.google.guava:guava:jar:29.0-jre:compile - version managed from 22.0; omitted for duplicate)

5. Prevent Accidental Transitive Dependencies

The maven-enforcer-plugin provides many built-in rules that simplify the management of a multi-module project. One of them bans the use of classes and methods from transitive dependencies.

Explicit dependency declaration removes the possibility of version collision of artifacts. Let's add the maven-enforcer-plugin with that rule to our parent pom:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>3.0.0-M3</version>
    <executions>
        <execution>
            <id>enforce-banned-dependencies</id>
            <goals>
                <goal>enforce</goal>
            </goals>
            <configuration>
                <rules>
                    <banTransitiveDependencies/>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

As a consequence, we must now explicitly declare the com.google.guava artifact in our project-collision module if we want to use it ourselves. We must either specify the version to use, or set up dependencyManagement in the parent pom.xml. This makes our project more mistake proof, but requires us to be more explicit in our pom.xml files.

6. Conclusion

In this article, we've seen how to resolve a version collision of artifacts in Maven.

First, we explored an example of a version collision in a multi-module project.

Then, we showed how to exclude transitive dependencies in the pom.xml. We looked at how to control dependencies versions with the dependencyManagement section in the parent pom.xml.

Finally, we tried the maven-enforcer-plugin to ban the use of transitive dependencies in order to force each module to take control of its own.

As always, the code shown in this article is available over on GitHub.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!