Making Sense of Maven Dependency Management

Anyone who has done any Java development in the past 10 years or so, have probably used Maven to build your applications. If you’re old enough to remember building things with Ant, you know that Maven dependency management is a definite improvement over manually managing your application’s libraries.

But over all these years that I’ve been developing with Java, I’ve noticed that people tend to get tripped up because they don’t really understand how Maven dependency management really works. The rules are actually quite simple. Once you understand them, Maven seems a lot less scary.

Closest Dependency Wins

The main thing to understand is how Maven resolves transitive dependencies. Transitive dependencies are the libraries used by the dependencies you declare in your project. For example, your project may depend on library A, but library A might require library B and library B might, in turn, requires library C. In this scenario, B and C are known as transitive dependencies. You didn’t include them directly, but they’re pulled in because they’re required by a library you need.

The problem occurs when there are multiple paths to the same dependency, but those paths result in different versions. For example, suppose you have a dependency tree that looks something like this:

maven dependency tree example

In this example, there are two paths to library C. Library A depends on C version 1.0 and B depends on D which depends on C version 2.0.

Which version does Maven include? 1.0 or 2.0?

If you answered 2.0, you wouldn’t be the first, but you’d be wrong.

Maven doesn’t use the highest numbered version, as you might expect. Instead, it chooses the closest dependency. In this example, C version 1.0 is only 2 steps away, whereas version 2.0 is three steps.

What happens if the conflicting dependencies are equidistant from your application? Starting with Maven 2.0.9, the first dependency which declares the transitive dependency wins. In other words, the order in which the conflicting transitive dependencies are encountered in the POM file breaks the tie.

The solution, in either case, is to include a reference the transitive library directly in the application’s POM file. This puts the library zero steps away, overriding any transitive dependencies, and removing the need to play games with the ordering of your dependencies.

Dependency Management

It’s all fine and good to include a dependency in your application’s POM file, but what if your dependencies change over time and they no longer need that library? Using our previous example, what if C is no longer a dependency of anything? By declaring a direct dependency on C, you’re including in every build of your application, even though you don’t really need it!

Fortunately, Maven has a way to handle this situation. The “dependencyManagement” section defines the versions of libraries to use, but only if they happened to be included. This means that, if none of the declared dependencies actually require the library, it isn’t included in the build.

1
2
3
4
5
6
7
8
9
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.foo</groupId>
<artifactId>C</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
</dependencyManagement>

Often this is used in parent POM file to enforce particular versions of dependencies across multiple modules. But it is also extremely useful for handling conflicts when resolving transitive dependencies.

To summarize, Maven uses the following rules to resolve dependencies, order of decreasing precedence.

  1. Explicitly declared dependency
  2. dependencyManagement version, if defined
  3. Closest version (POM order breaks ties)

When in Doubt, Ask

If you suspect that you have a transitive dependency conflict, the first thing to do is use the dependency plugin:

1
$ mvn dependency:tree -Dverbose

This will show all the dependencies of your project and how they were derived. The verbose option indicates that the dependency conflicts should be shown. Otherwise it just shows the final dependency selected. If we were to run this on our earlier example, it would indicate that library C was pulled in by the dependency on A.

1
2
3
4
5
6
7
[INFO\] [dependency:tree]
[INFO\] com.myproject:my-app:1.0-SNAPSHOT
[INFO\] +- com.myproject:A:jar:1.0:compile
[INFO\] | \- com.foo:C:jar:1.0:compile
[INFO\] \- com.myproject:B:jar:1.2:compile
[INFO\] \- com.myproject:D:jar:1.5:compile
[INFO\] \- (com.foo:C:jar:2.0:compile - omitted for conflict with 1.0)

When in doubt, if you have a transitive dependency conflict, add a dependencyManagement section and set the version explicitly.

Worth Learning

Maven is a powerful tool that makes building projects extremely easy and uniform. Like any tool, it has it’s idiosyncrasies, but learning these oddities are almost always worth your while. There are other build tools out there with their own set of issues. Whichever tool you use, make sure you understand how it’s doing it’s job.

Hopefully this has helped dispel some of the mystery around how Maven dependency management works. Knowing how your build system works will make it easier to get back to the job at hand: writing useful code to solve real user problems.

Question: What is the one thing that you find difficult to understand about Maven?