Using Bill of Materials to Simplify Dependency Management

Featured image for sharing metadata for article

One of the projects I maintain, cucumber-reporting had recently broken if trying to upgrade one of its dependencies, cucumber-jvm, as it didn't match the pinned dependency the project used for gherkin.

This was because the new cucumber-jvm version depended on a new version of gherkin. In this case, because the versions were strongly linked, it was impossible to upgrade them independently.

To resolve this, I needed to dig through the dependency trees cucumber-jvm, determine which version of gherkin was required, and upgrade it, which was fortunately not too difficult - but could be much more work if multiple packages could've been affected.

Because both are produced by the Cucumber project, and are tied so closely, having an easier way of managing this would be ideal, rather than requiring library consumers to work it out themselves. Fortunately the Cucumber project has added Bill of Materials support as part of an upcoming release, and will make this simpler.

What is a Bill of Materials (BOM?)

The Bill of Materials concept is a Maven module that produces a set of dependencies and their versions as a POM file. If you've used Maven, you may recognise this from a "parent project", but BOMs work across different build tools from the same Maven POM.

This allows a client to consume a set of dependencies from your project, just by importing the BOM into their own project. Consumers can then avoid specifying versions of libraries produced by the BOM, instead allowing the BOM to control that - but can upgrade dependencies if required.

Why?

As mentioned, this can lead to being able to manage the versions across inter-dependent versions of libraries. I've been using this pattern for a while with Spring Boot, and it allows the Spring Boot project to manage the dependency tree more centrally, ensuring that a set of libraries definitely all work together, and providing a "safe" set of dependencies for teams to pull into their own projects.

One of my colleagues James has been using BOMs for some time to manage dependencies across multiple microservices, pulling things like Spring Boot, testing libraries, and internal tooling all in one place.

But aside from central management across different types of libraries, there's also the benefit for dependencies that you publish yourself. Similar to Cucumber producing a BOM for all of their versions, we've seen this with a few libraries at work that have multiple submodules.

This means that as a consumer of these libraries, you need to add the libraries to your dependencies list, specifying the version potentially in multiple places, and generally being a bit more duplication than we'd hope.

With the BOM solution, we can instead import i.e. me.jvt.hacking:internal-library-bom:1.2.3 and then have the ability to import the -core, -test, etc libraries easily, all pinned to v1.2.3.

With Gradle

Using the Java Platform Plugin in Gradle, we can produce a submodule:

project(':internal-library-bom') {
  apply plugin: 'java-platform'

  afterEvaluate { // to make sure everything else has applied first, i.e. dynamic versioning
    dependencies {
      constraints {
        rootProject.subprojects.findAll { it != project }.each { api(it) }
      }
    }
  }

  publishing {
    publications {
      thePlatform(MavenPublication) {
        from components.javaPlatform
      }
    }
  }
}

Note that because we have a handy Groovy DSL, we can programatically add every submodule (that isn't the -bom submodule) to the BOM, rather than listing them manually - no doubt accidentally leaving things out in the future!

With Maven

With Maven, we add a new Maven submodule, which produces a pom, with a dependencyManagement block that lists all dependencies manually:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>me.jvt.hacking</groupId>
  <artifactId>internal-library-bom</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>me.jvt.hacking</groupId>
        <artifactId>internal-library-core</artifactId>
        <version>${project.version}</version>
      </dependency>
      <dependency>
        <groupId>me.jvt.hacking</groupId>
        <artifactId>internal-library-test</artifactId>
        <version>${project.version}</version>
      </dependency>
      <dependency>
        <groupId>me.jvt.hacking.another</groupId>
        <artifactId>dependent-library</artifactId>
        <version>2.1</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#java #blogumentation #gradle #maven.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.