Manage JAVA dependencies with confidence using Maven BOMs

Brian NQC
6 min readJul 9, 2023

--

Introduction

If you have used Spring Boot, you would likely notice that we just need to declare the libraries we want to use without having to specifying their versions. So, how could build tools like Gradle and Maven resolve the versions of these dependencies and ensure that they are compatible with each other?

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'javax.cache:cache-api'
implementation 'jakarta.xml.bind:jakarta.xml.bind-api'

runtimeOnly 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

The answer for all of this magic is Maven Bill Of Materials (BOM). In this article, I will introduce Maven BOM and how we can leverage it to get rid of annoying and hard-to-resolve exceptions such as ClassNotFoundException and NoSuchMethodError in our Java and Kotlin projects.

Source: https://blog.gradle.org/alignment-with-gradle-module-metadata

Problem statement: Newer versions are not usually the right versions

Let’s take a look at this imaginary example: Team A develops project app which uses guava:31.1 and a library named lib. lib can be a home-growth or any off-the-shelf library.

Project Lib:

public class FutureStub {

public static Future<String> ofString(String s) {
doSomethingFunOnPurpose();
return CompletableFuture.completedFuture(s);
}

private static void doSomethingFunOnPurpose() {
Futures.immediateCheckedFuture(new Object());
}
}

Project App:

public class App {

public static void main(String[] args) throws Exception {
final var future = FutureStub.ofString("Hello Maven BOM");
System.out.println(future.get());
}
}

When we run app, instead of printing “Hello Maven BOM”, it throws java.lang.NoSuchMethodError. Why does this happen? Method Futures.immediateCheckedFuture() was removed in Guava version 28.0, but the code is being executed with Guava version 31.1. This mismatch results in a fatal error when trying to execute the code.

When introducing a new dependency into our project, we often opt for the latest version without thoroughly considering its compatibility with existing dependencies and their transitive dependencies. Even if we are aware of potential conflicts, finding the right combination can be a daunting task. This challenge is amplified in real-world scenarios where Java projects commonly rely on numerous libraries, which in turn bring along a multitude of transitive dependencies. Ensuring compatibility among them is far from an easy task, especially considering that these incompatibilities may only occurs at runtime, making debugging a complex endeavor.

Solution: Maven BOM

What is Maven BOM?

Abdelbaki BEN ELHAJ SLIMENE has a very good definition on his blog. I simply quote his words mark the most important parts bold.

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.

Adopt well-tested BOMs

Utilizing well-tested BOMs is a simple and effective method to enhance confidence when working with dependencies in Java projects. Renowned libraries like Jackson, Akka, Cucumber, and Spring provide their own BOMs, streamlining the process of selecting and managing compatible versions of dependencies.

Spring BOMs

As one of the biggest Java communities, Spring provides many good resources for Java developers, even for those who don’t use Spring Framework. There are some BOMs are published by the Spring team, the most well-known one is called Spring Boot Dependencies. This BOM brings hundreds of commonly used libraries such as fasterxml.jackson, com.squareup.okhttp3, io.micrometer, io.netty, io.prometheus, org.junit.jupiter, org.mockito, org.assertj and many many more. This BOM is included in Spring Boot projects by default. If you are not using Spring Boot, you can still be beneficial of this massive dependency pool. More about it in later section.

Define our own BOMs

Teams often develop reusable libraries to be shared across organizations, allowing client teams to select the specific libraries they need. For instance, Jackson offers a range of libraries such as jackson-core, jackson-databind, jackson-annotations, and more. To ensure compatibility, Jackson provides jackson-bom to simplify library selection. Similarly, if you publish libraries for other teams, creating a BOM can enhance integration and streamline development.

With Gradle

With the help of Java Platform Plugin, creating a Maven BOM using Gradle is rather simple.

plugins {
id 'java-platform'
id 'maven-publish'
}

javaPlatform {
allowDependencies() // This allows importing other BOMs
}

dependencies {
api platform('org.springframework.boot:spring-boot-dependencies:2.6.4')
constraints {
api('group-01:module-01:1.1.0')
api('group-01:module-02:1.1.0')
api('group-02:module-01:2.3.1')
api('group-02:module-02:2.3.1')
}
}

With Maven

Maven supports BOM natively, we can simply declare BOMs and other dependencies in dependencyManagement block.

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>group-01</groupId>
<artifactId>module-01</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>group-01</groupId>
<artifactId>module-02</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>group-02</groupId>
<artifactId>module-01</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>group-02</groupId>
<artifactId>module-02</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
</dependencyManagement>

Ensure compatibility among BOMs

When multiple BOMs are used, we need to pick the versions which are compatible. There is a simple and effective approach: choose BOMs with overlaps, e.g.

BOM 1:
group-0.artifact-0:1.2.3 <= Appears in both
group-0.artifact-1:2.2.0
group-1.artifact-0:1.0.3
group-2.artifact-1:1.1.0 <= Appears in both
group-2.artifact-0:2.0.0

BOM 2:
group-0.artifact-0:1.2.3 <= Appears in both
group-2.artifact-0:2.0.0 <= Appears in both
group-3.artifact-0:3.0.0
group-3.artifact-1:3.0.0

Use Maven BOMs in Java projects

For any build tools, importing Maven BOMs in Java projects is relatively simple.

Gradle v6+

From v6, Gradle supports importing Maven BOM out-of-the-box with implementation(platform(…))

dependencies {
// Add BOMs here
implementation(platform('org.springframework.boot:spring-boot-dependencies:2.6.4'))

// Then, now we can simply declare the dependencies we need
// without having to worry about their versions
implementation 'org.apache.kafka:connect-api'
implementation 'com.rabbitmq:amqp-client'
}

Spring Dependency Management Plugin

When an older version of Gradle is used, or when Spring Dependency Management Plug is already applied, e.g. in Spring Boot projects, we can leverage such plugin to use BOMs.

plugins {
id "io.spring.dependency-management" version <<version>>
}

dependencyManagement {
imports {
// The plugin already brings spring-boot-dependencies BOM,
// We can also add other BOMs like:
mavenBom 'another:dependency-bom:2.0.0'
}
}

dependencies {
// Then, now we can simply declare the dependencies we need
// without having to worry about their versions
implementation 'org.apache.kafka:connect-api'
implementation 'com.rabbitmq:amqp-client'
}

Maven

The XML syntax of Maven might be a little verbose. However, it makes using BOM in maven project very simple as well.

<dependencyManagement>
<dependencies>
<!-- Declare the BOMs we want to use here -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Then add dependencies we want -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>connect-api</artifactId>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
</dependency>
<dependencies>

Alternative solution: Gradle with Platforms and Module Metadata

Managing dependencies in any languages is a real headache. Therefore, many teams are trying to tackle this problem in different ways. In Java project, Gradle with Platforms and Module Metadata is another considerable solution.

If you find this article helpful, please motivate me with one clap. You can also checkout my other articles at https://medium.com/@briannqc and connect to me on LinkedIn. Thanks a lot for reading!

--

--

Brian NQC

Follow me for contents about Golang, Java, Software Architecture and more