Maven Dependency Mechanisms
Whenever we use Maven as our build tool in our project, have we ever thought how Maven resolves dependency conflicts? Probably not, since Maven just automatically resolves them most of the time. In this article, we will learn the behind the scenes on how Maven resolves them.
Maven Dependency Mechanisms
So let’s start off with a question - what is Maven’s dependency mechanism?
Apache states that:
Dependency management is a core feature of Maven. Managing dependencies for a single project is easy. Maven helps a great deal in defining, creating, and maintaining reproducible builds with well-defined classpaths and library versions.
In other words, dependency management simply maintains reproducible builds and that includes resolving version conflicts which we are about to explore in this article.
2. Dealing with Multiple Versions from the Dependencies
Majority of our projects do have this kind of complicated relationship between our dependencies and in order for Maven to maintain the correct dependency versions, it uses a method called
Transitive Dependency wherein it applies basic transitivity (if dependency A => B, and B => C then A => C).
In the following section, we will explore Maven’s approaches and features that helps resolve those
2.1. Dependency Mediation
Maven has this approach that determines what version of an artifact will be selected when multiple versions are encountered as dependencies.
The diagram below is an example of a dependency having a conflict in version.
Since Maven transitively loads all dependencies from the Pom file, the dependency
Log4j-1.2.2 will be included in the dependency tree despite
Log4j-1.2.3 exists(and has a higher version!). To resolve this conflict, Maven uses strategies to mediate dependencies:
2.1.1. Strategy #1: Using Nearest Definition
Maven uses the closest distance from the root(the Pom file) to a specific dependency to resolve dependency conflict. This is Maven’s default strategy in resolving dependency conflicts when building our project.
Suppose our dependencies with conflict are located in different levels of the project.
Since the nearest definition for the dependency
Log4j is version
Log4j-1.2.3 in comparison with
Log4j-1.2.3 will be used as the dependency of this project. And if two dependency versions are located at the same depth in the dependency tree, the first declaration will be selected.
2.1.2. Strategy #2: Using Concrete Versions
Maven switches to another strategy as soon as it encounters a concrete version number(version number with square brackets[inclusive] / parenthesis[exclusive]) in one of the dependencies that are in conflict.
For instance, if the dependency
commons-logging has a concrete version, Maven will automatically use this concrete version to resolve the conflict.
But if the dependency with a concrete version is not available from the repository, it will propagate an error and fail.
Another scenario wherein Maven uses the same strategy is when the concrete version is in a range such as this:
[1.2.2,1.2.5], how does Maven resolve this conflict?
Since the nearest dependency version is within the scope of the concrete version number(
1.2.3 is within the range of
1.2.5) , Maven will instead select
2.2. Dependency management
Maven allows the author of the project to inject the dependency versions of artifacts to be used when they are encountered in transitive dependencies. And it also automatically resolves those dependencies that have no version by looking for other versions in the dependency tree.
2.3. Dependency scope
Maven lets the developers to manually limit the transitivity of a dependency and to determine when a dependency is included in a classpath. Currently, there are 5 scopes available in Maven:
The default scope if none is specified. Dependencies with this scope are available in all classpath of a project.
Expect the hosting environment such as an application server (examples are Tomcat and Websphere Application Server) to provide the dependencies at runtime.
Includes a dependency with this scope in the runtime and test classpath, but not the compile classpath.
Indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases. This scope is not transitive.
It indicates the dependency is to be replaced with the effective list of dependencies in the specified Pom’s
<dependencyManagement> section. Since they are replaced, dependencies with a scope of
import do not actually participate in limiting the transitivity of a dependency.
This scope is only supported on a dependency of type Pom in the
2.4. Excluded dependencies
Another feature that Maven is capable of is granting the author of the project to explicitly exclude specific dependencies using the tag
With the diagram above, since
commons-logging has an
exclusion tag for its child dependency
Log4j, Maven will not include
Log4j dependency during Build, and will instead use the other
2.5. Optional dependencies
Lastly, Maven can exclude the dependency by default and requires to explicitly include the dependency. To simply put, optional dependencies are “excluded by default”.
Consider the diagram below:
Project A has a dependency to Project B but it is set to
optional. When Project X, that has a dependency to Project A attempts to reference Project B, it cannot get Project B’s reference because the tag
optional prevents it from being transitively included in Project X. If the developer wants to reference Project B in Project X, the
optional tag should be removed.
We now know that Maven has different approaches used to resolve dependency version conflicts. It also has features that lets developers address the transitivity of the dependencies. I hope this article helps clear things up on how Maven resolves dependency version conflict.
For other topics related to Maven, you can read their documentation in here.