Manage your inner conflicts

If you are like me, you will try to keep all the dependencies of your projects up to date, but, if you do it blindly, sooner or later, you will probably run into problems.

Each dependency that we include in our projects might link to other artifacts. If you use Maven as your dependency manager then it will automatically pull these artifacts, also called transitive dependencies.

Version Collision of Artifacts

Version collision happens when multiple dependencies link to the same artifact but use different versions.

As a result, there may be errors that can occur in our applications both in the compilation phase or, even worst, at runtime.

Showcasing the problem

Let’s assume that we have a following except on a POM file in one of our projects.

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.5</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>5.4.5</version>
    </dependency>
</dependencies>

If we run the following command:

mvn dependency:tree -Dverbose -Dincludes=org.springframework:spring-beans

The output will include:

[INFO] +- org.springframework:spring-context:jar:5.3.5:compile
[INFO] |  +- org.springframework:spring-aop:jar:5.3.5:compile
[INFO] |  |  \- (org.springframework:spring-beans:jar:5.3.5:compile - omitted for duplicate)
[INFO] |  \- org.springframework:spring-beans:jar:5.3.5:compile
[INFO] \- org.springframework.security:spring-security-web:jar:5.4.5:compile
[INFO]    +- org.springframework.security:spring-security-core:jar:5.4.5:compile
[INFO]    |  \- (org.springframework:spring-beans:jar:5.2.13.RELEASE:compile - omitted for conflict with 5.3.5)
[INFO]    +- (org.springframework:spring-beans:jar:5.2.13.RELEASE:compile - omitted for conflict with 5.3.5)
[INFO]    \- org.springframework:spring-web:jar:5.2.13.RELEASE:compile
[INFO]       \- (org.springframework:spring-beans:jar:5.2.13.RELEASE:compile - omitted for conflict with 5.3.5)

As we can see there are two different versions (5.2.13.RELEASE and 5.3.5) of the spring-beans artifact in the dependency tree. Which one will be chosen by Maven to be included in your project?

The answer, in this case, is 5.3.5.

Now, let’s switch the order of the dependencies definitions:

<dependencies>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>5.4.5</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.5</version>
    </dependency>
</dependencies>

And if we run the same command again to check the dependency tree the output will be:

[INFO] +- org.springframework.security:spring-security-web:jar:5.4.5:compile
[INFO] |  +- org.springframework.security:spring-security-core:jar:5.4.5:compile
[INFO] |  |  \- (org.springframework:spring-beans:jar:5.2.13.RELEASE:compile - omitted for duplicate)
[INFO] |  +- org.springframework:spring-aop:jar:5.2.13.RELEASE:compile
[INFO] |  |  \- (org.springframework:spring-beans:jar:5.2.13.RELEASE:compile - omitted for duplicate)
[INFO] |  +- org.springframework:spring-beans:jar:5.2.13.RELEASE:compile
[INFO] |  \- org.springframework:spring-web:jar:5.2.13.RELEASE:compile
[INFO] |     \- (org.springframework:spring-beans:jar:5.2.13.RELEASE:compile - omitted for duplicate)
[INFO] \- org.springframework:spring-context:jar:5.3.5:compile
[INFO]    \- (org.springframework:spring-beans:jar:5.3.5:compile - omitted for conflict with 5.2.13.RELEASE)

Now, the version of the spring-beans that is included is the 5.2.13.RELEASE.

How does Maven resolve version conflicts?

The first thing to know is that Maven can’t sort versions: The versions are arbitrary strings and may not follow a strict semantic sequence. That means that Maven doesn’t know which version is newer or older and cannot choose to always take the newest version.

Instead, Maven uses the concept of dependency mediation, which resolves the version conflicts by choosing the nearest definition.

How dependency mediation works

“Nearest definition” means that the version used will be the closest one to your project in the tree of dependencies. Consider the following tree of dependencies:

  A
  ├── B
  │   └── C
  │       └── D v3.0 (ommited: not nearest in depth)
  └── E
  │   └── D v2.0     (picked: nearest in depth and first in resolution order)
  └── F
      └── D v1.0     (ommited: not the first in resolution order)

Going back to the dependency tree results shown above, Maven indicates for each transitive dependency why it was omitted:

  • “omitted for duplicate” means that Maven preferred another dependency with the same name and version over this one (i.e. the other dependency had a higher priority according to the resolution order and depth)
  • “omitted for conflict” means that Maven preferred another dependency with the same name but a different version over this one (i.e. the other dependency with the different version had a higher priority according to the resolution order and depth)

There must be a better way

Relying on this automatic process can have unpredictable results if we aren’t careful or don’t have a full understanding of what is happening behind the curtains (that now we have 😜).

What if we want to resolve the dependency conflict ourselves?

We can always use the version we’re hoping to use, even a transitive one, by specifying it in the POM, basically, turning it into a direct one.
That is one way to resolve the problem but could lead to cluttering our POM because will be hard to decide how many need to be mentioned.

Or we could do even better and let dependency management take control.

Override a Transitive Dependency version using the dependencyManagement section

dependencyManagement section contains dependency elements. Each dependency is a lookup reference for Maven to determine the version to select for transitive (and direct) dependencies. The version of the dependency is mandatory in this section.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.2.13.RELEASE</version>
        </dependency>
    </dependencies>
</dependencyManagement>

However, outside of the dependencyManagement section, we now can omit the version of our dependencies, and Maven will select the correct version of the transitive dependencies from the list of dependencies provided in dependencyManagement.

We should note that defining a dependency in the dependencyManagement section doesn’t add it to the dependency tree of the project, it is only used just for lookup reference.

Introducing Maven’s Bill of Material (BOM)

The Bill Of Material is a special POM file that groups dependency versions that are known to be valid and tested to work together. This will reduce the developers’ pain of having to test the compatibility of different versions and reduce the chances to have version mismatches.

The BOM file has:

  • pom packaging type: <packaging>pom</packaging>.
  • dependencyManagement section that lists the dependencies of a project.

Using a BOM as a Parent POM

A good example of this is the standard way of creating Spring Boot projects, where the base POM should have spring-boot-starter-parent has its parent, which, in its turn, inherits from spring-boot-dependencies. This POM file has a dependencyManagement section containing all the dependencies required to run a Spring Boot project. This BOM file is maintained by the Spring Boot team, and, with each new version of Spring Boot, a new BOM will be provided that handles version upgrades and makes sure that all the given dependencies work well together. Developers will only care about upgrading the Spring Boot version since the underlying dependencies compatibility was already tested by the Spring Boot team.

Conclusion

Understanding dependency management in Maven is crucial to avoid getting version conflicts and wasting time resolving them. Using the BOM is a good way the ensure consistency between the dependencies versions and a safer way in multi-module project management.

Pop quiz

Besides using a BOM as Parent POM there is another way to use a BOM in a project. Do you know how? What is the main reason to use it?

One comment

Comments are closed.

Discover more from Engineering @ Rho

Subscribe now to keep reading and get access to the full archive.

Continue reading