Publish BOMs for multi-module projects

When we build libraries for others to use, we are often in the fortunate position where our work is entirely encapsulated within a single library. This makes communicating to users of our library about which version to use, when to upgrade, and how to deal with version conflicts much simpler. Sometimes though our projects aren't containable within a single library. Good examples that many people will be familiar with are the Spring, Micronaut, and Quarkus projects. Even the Azure SDK for Java ships separate libraries for different Azure services. The argument why projects do this is to reduce the surface area of a library into more comprehensible chunks of functionality, and by doing so making consumers of the library more productive and less overwhelmed with API and unnecessary dependencies.

Elsewhere on this website the guidance for Java developers is to use BOMs whenever available, with the reasoning being that it offloads the burden on the user to track the versions of their dependencies, and to ensure that their choice of version for each dependency is going to work well together. Any long-time Java developer will tell you that there is no joy in trying to match together dependencies so that their transitive dependency closure is able to work well together without runtime exceptions or other unexpected behavior.

How do I create a BOM?

A BOM is merely another artifact that we need to get into the habit of publishing to Maven Central, except it doesn't have any Java source code attached to it. Here's a short snippet of the Azure SDK BOM:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.azure</groupId>
  <artifactId>azure-sdk-bom</artifactId>
  <version>1.0.4-beta.1</version>
  <packaging>pom</packaging>
  <name>Azure Java SDK BOM (Bill of Materials)</name>
  <description>Azure Java SDK BOM (Bill of Materials)</description>
  <url>https://github.com/azure/azure-sdk-for-java</url>
  <licenses>
    <license>
      <name>The MIT License (MIT)</name>
      <url>http://opensource.org/licenses/MIT</url>
      <distribution>repo</distribution>
    </license>
  </licenses>
  <developers>
    <developer>
      <id>microsoft</id>
      <name>Microsoft Corporation</name>
    </developer>
  </developers>
  <scm>
    <connection>scm:git:git://github.com/azure/azure-sdk-for-java</connection>
    <developerConnection>scm:git:git://github.com/azure/azure-sdk-for-java</developerConnection>
    <url>https://github.com/azure/azure-sdk-for-java</url>
  </scm>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <packageOutputDirectory>${project.build.directory}</packageOutputDirectory>
  </properties>

  <issueManagement>
    <system>GitHub</system>
    <url>https://github.com/azure/azure-sdk-for-java/issues</url>
  </issueManagement>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.azure</groupId>
        <artifactId>azure-core</artifactId>
        <version>1.17.0</version>
      </dependency>
      <dependency>
        <groupId>com.azure</groupId>
        <artifactId>azure-core-http-netty</artifactId>
        <version>1.10.0</version>
      </dependency>
      <dependency>
        <groupId>com.azure</groupId>
        <artifactId>azure-cosmos</artifactId>
        <version>4.16.0</version>
      </dependency>
      <dependency>
        <groupId>com.azure</groupId>
        <artifactId>azure-storage-blob</artifactId>
        <version>12.12.0</version>
      </dependency>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

This is a standard BOM file - you'll observe that it is configured exactly the same as a standard Maven POM, and that is because this is all it is! The only major distinction is that the packaging value is pom, rather than the more common jar.

Releasing a BOM uses the same release processes that we use today for releasing any other artifact. Maven Central will accept a BOM without any additional configuration.

BOM Challenges

What to include

There are different ways to approach a BOM. We could consider ourselves the owner of the universe of libraries, adding every known library we can into our BOM, even if they fall outside of our ownership as library developers. Alternatively, we could only include libraries in our BOM that are directly developed by ourselves. There are pros and cons to each approach, but from my experience, it is better to consider a BOM a look-up for libraries that only you provide. Going beyond this and including other libraries has a tendency to result in classpath collisions at runtime that is undesirable, and also often hard to resolve.

Versioning

Before you start to release a BOM it is important to consider how it will be versioned. Because a BOM represents many libraries, there are many policies on how to version a BOM. In particular, consideration must be given to whether the BOM follows semantic versioning and increments its major version any time a listed dependency within the BOM has a major version increment.

Because users depending on a BOM will be offloading the responsibility of tracking individual dependency versions, it seems appropriate in most cases that a major version increment in any constituent dependency would be reflected in a major version increment of the BOM itself. This allows us to inform users implicitly that the next BOM upgrade they have available to them brings with it breaking changes that they must consider carefully.

The challenge of this approach is if the versioning strategy of the constituent libraries is so out of sync that the BOM is forced to undergo major version revisions on a frequent basis. This situation speaks to a larger organizational problem that should be worked through, which is discussed below.

Release concerns

A BOM is intended to unify libraries from a single project under a common banner to help our users be more productive. Sometimes these libraries can be developed by separate teams whose release frequencies are not as in sync as ideal. This can lead to pressures on when and how to release a BOM. For example, questions we will need to answer include:

  • Should a BOM release occur on a regular cadence or whenever a constituent library is updated?
  • Should we include dependencies in our BOM that are in development (e.g. that are considered alpha, beta, preview, RC, or equivalent)?
  • How do we reconcile dependencies in our BOM if the transitive closure of those dependencies does not fully close on a single version of each dependency? Do we remove dependencies between releases of the BOM if we cannot get to a clean dependency graph?

Teams will arrive at different answers for these questions, but to give direction based on experiences here at Microsoft, we have decided on the following:

  • BOMs will be released regularly. We decided to release monthly, as this aligned with our monthly release cadence for all Azure SDK for Java client libraries.
  • We decided to not include in-development libraries in our BOM.
  • We wrote tooling that keeps us informed constantly on the state of the transitive closure of our next BOM release. We try our best to ensure that the next release of all Azure SDK for Java client libraries aligns on their dependency versions. However, if we cannot fully get to a state that is clean, we never remove a dependency from the BOM, as this will have the effect of breaking people who upgrade to a new BOM and find that a dependency version cannot be determined any longer.

Communication

Once a BOM is released, the next challenge is communicating its availability to all developers. You preferentially want developers to use this in all projects, and you never want to see developers specifying versions of dependencies that are listed within the BOM. In all places where Maven (or Gradle, etc) coordinates are listed for a library that you own, you want to preface it by encouraging users to instead use the BOM instead. For example, you might do something similar to what is documented for the azure-cosmos library here.

Conclusion

In summary, a BOM is not a trivial amount of work, particularly if you want to improve the lives of developers using your libraries. A BOM should be viewed as a means of reducing developer friction, offloading burden from them and moving it onto yourself. When we remove friction like this, we are more likely to delight our users, and enable them to become advocates for our libraries and any services that they represent.