BOM (Bill Of Materials)

BOM (Bill Of Materials)
Photo by Mark Boss / Unsplash

What is BOM?

To solve version mismatch issue, you can use the concept of a “bill of materials” (BOM) dependency. A BOM dependency keep track of version numbers and ensure that all dependencies (both direct and transitive) are at the same version.

Many great frameworks such as Spring Boot, Grails or Micronaut are using Maven bill-of-material (BOM) to manage versions of their dependencies aligned. Using BOM you can constraint the versions of transitive dependencies as well as it allows you to specify just the group and name of the module and let the version be determined by BOM.

Why dependencies conflict?

Maven avoids the need to discover and specify the libraries that your own dependencies require by including transitive dependencies automatically. There is no limit to the number of levels that dependencies can be gathered from. With transitive dependencies, the graph of included libraries can quickly grow quite large. For this reason, there are additional features that limit which dependencies are included:

  • Dependency mediation - this determines what version of an artifact will be chosen when multiple versions are encountered as dependencies. Maven picks the "nearest definition". That is, it uses the version of the closest dependency to your project in the tree of dependencies. Note that if two dependency versions are at the same depth in the dependency tree, the first declaration wins.
  • Dependency management - this allows project authors to directly specify the versions of artifacts to be used when they are encountered in transitive dependencies or in dependencies where no version has been specified.
  • Dependency scope - this allows you to only include dependencies appropriate for the current stage of the build.
  • Excluded dependencies - If project X depends on project Y, and project Y depends on project Z, the owner of project X can explicitly exclude project Z as a dependency, using the "exclusion" element.
  • Optional dependencies - If project Y depends on project Z, the owner of project Y can mark project Z as an optional dependency, using the "optional" element.

Consider the following dependency scenarios:

  • Project A depends on B 2.1.3 and C 1.2.0 versions;
  • B 2.1.3 depends on D 1.1.6;
  • C 1.2.0 depends on D 1.3.0.

In the above example, Project A's dependency on D conflicted. According to the maven dependency mediation rule, the version 1.1.6 (the principle of proximity) that came into effect last.

In this case, C depends on version 1.3.0 of D but it does take effect at runtime is version 1.1.6, so it is easy to cause problems at runtime, such as NoSuchMethodError, ClassNotFoundException, etc.

A pom.xml file indeed

POM is an acronym for Project Object Model. The pom.xml file contains information of project and configuration information for the maven to build the project such as dependencies, build directory, source directory, test source directory, plugin, goals etc. It behaves just like build.gradle file in Gradle.

BOMs are ordinary pom.xml files — they contain no source code and their only purpose is to declare their bundled modules. It defines the versions of all the artifacts that will be created in the library. Other projects that wish to use the library should import this pom into the dependencyManagement section of their pom.

Maven provides a tag <dependencyManagement> to realize the functionality of BOM. You need to add the maven bom information in this tag as follows. Here take the example of importing Firebase bom file in Maven (also by this, we can merge multiple bom files into one), and then we can no longer need to specify the version attribute when depending on Firebase artifacts.

<dependencyManagement>
    <dependencies>
      <dependency>
       <groupId>com.google.firebase</groupId>
       <artifactId>firebase-bom</artifactId>
       <version>25.1.0</version>
       <type>pom</type>
       <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

And Here is the example of specifying versions in BOM.

<dependencyManagement>
<dependencies>
  <dependency>
      <groupId>com.glow.android</groupId>
      <artifactId>AndroidJSCore</artifactId>
      <version>1.3</version>
    </dependency>
    <dependency>
      <groupId>com.inmobi.monetization</groupId>
      <artifactId>inmobi-ads</artifactId>
      <version>7.3.0</version>
    </dependency>
</dependencies>

Create a BOM

Besides relying on platform BOMs, we can also use a customized BOM File to manage more dependency versions, where actually we manage all the dependencies shared by our apps, without trouble to bump versions in each place.

Via repo: /upwlabs/Base-Android/bom-seed, we can build the BOM. Here are steps:

  1. (Optionally) declare versions placeholders such as prime.version in file src/main/versions/_versions.properties
  2. Declare desired module version in a file src/main/versions/{$groupId}.properties (for example src/main/versions/com/glow/android.prime.properties), in format of $artifactId = $version, either explicitly such as prime = 4.1.0 or using the placeholder defined before prime = prime.version
  3. If you want to import a platform BOM, declare the bom version in a file src/main/boms/{$groupId}.properties.
  4. Push a tag (need in format of bom/*, such as bom/1.0.0) to the repository to publish a new version of the BOM (don't forget bump versions)

Special attention: to better simplify the directory structure and group files, the groupId is split into dir names and file names. Plz make sure the correct file path before adding it.

Using BOM

Gradle provides support for importing BOM files. The BOM support in Gradle works similar to using <scope>import</scope> when depending on a BOM in Maven. In Gradle however, it is done via a regular dependency declaration on the BOM:

dependencies {
 	// import a BOM
	implementation platform('com.glow.android.prime:bom:1.0.3')

	// define dependencies without versions
	implementation 'com.google.code.gson:gson'	}

Gradle treats all entries in the <dependencyManagement> block of a BOM similar to Gradle’s dependency constraints. This means that any version defined in the <dependencyManagement> block can impact the dependency resolution result.

However often BOMs are not only providing versions as recommendations, but also a way to override any other version found in the graph. You can enable this behavior by using the enforcedPlatform keyword, instead of platform. In contrast, you can override the version in BOM by explicitly specifying it in your own build.gradle file if using platform.

Special attention: if you want to override the version in the BOM, which is imported in your own BOM, you need to specify it in the body of tag <dependencyManagement> in your own BOM instead of your build.gradle file.

Other problems

Plugins kapt, annotationProcessor don't support BOM, so we still have to add version info and please remember to refactor these places when upgrading relevant libs.

Other references

Here is the file listing the current versions of dependencies in all 4 apps: dependency management list(update to 2020/03/24).