ZetCode

Maven BOM

last modified November 8, 2026

In this article we show how to use a Maven BOM (Bill of Materials) to manage dependency versions in one place. The examples use Maven 3.10 and Java 25.

A BOM is a POM file with pom packaging whose dependencyManagement section lists dependencies with their versions. Other projects import it and then declare those dependencies without versions. A BOM does not add any dependency to a project; it only decides which version is used when the project, or one of its dependencies, needs the artifact.

BOMs bring several advantages:

Creating a BOM

Our BOM manages the versions of Jackson, Commons Lang, and Gson.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>example-bom</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.fasterxml.jackson</groupId>
                <artifactId>jackson-bom</artifactId>
                <version>2.23.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.21.0</version>
            </dependency>

            <dependency>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>2.15.0</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

The packaging must be pom; a BOM has no code.

<dependency>
    <groupId>com.fasterxml.jackson</groupId>
    <artifactId>jackson-bom</artifactId>
    <version>2.23.0</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

A BOM can import other BOMs. The Jackson BOM manages all Jackson modules, so our BOM covers them without listing each one.

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.21.0</version>
</dependency>

Single artifacts are listed with their versions.

Other projects get the BOM from a repository, so we install it into the local repository. Only the POM file is installed. In a company, the BOM would be deployed to a shared repository with mvn deploy.

$ mvn install
[INFO] Installing /home/user/bom/pom.xml to /home/user/.m2/repository/com/example/example-bom/1.0.0/example-bom-1.0.0.pom
[INFO] BUILD SUCCESS
...

Importing the BOM

A project imports the BOM in its dependencyManagement section with the pom type and the import scope.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>example-bom</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>
</project>

The import copies the managed dependencies of the BOM into the project. The two dependencies in the dependencies section have no versions; they come from the BOM.

src/main/java/com/example/App.java
package com.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;

import java.util.List;

public class App {

    record Library(String name, String version) {}

    public static void main(String[] args) throws Exception {
        var libs = List.of(
                new Library("jackson-databind", ObjectMapper.class.getPackage().getImplementationVersion()),
                new Library("commons-lang3", StringUtils.class.getPackage().getImplementationVersion()));

        for (var lib : libs) {
            System.out.println(StringUtils.rightPad(lib.name(), 20, '.') + " " + lib.version());
        }

        System.out.println(new ObjectMapper().writeValueAsString(libs));
    }
}

The application prints the versions of the libraries it uses, read from the manifests of their JAR files, and serializes the list to JSON with Jackson.

Both the direct dependencies and the transitive Jackson modules have the versions from the BOM. The Jackson annotations module is versioned as 2.23; since Jackson 2.20, it uses only the major and minor version numbers.

$ mvn dependency:tree
[INFO] com.example:app:jar:1.0.0
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.23.0:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.23:compile
[INFO] |  \- com.fasterxml.jackson.core:jackson-core:jar:2.23.0:compile
[INFO] \- org.apache.commons:commons-lang3:jar:3.21.0:compile
...
$ mvn -q compile exec:java -Dexec.mainClass=com.example.App
jackson-databind.... 2.23.0
commons-lang3....... 3.21.0
[{"name":"jackson-databind","version":"2.23.0"},{"name":"commons-lang3","version":"3.21.0"}]

Overriding a BOM version

A project can override a version from the BOM by managing the dependency itself.

pom.xml (dependency management)
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>example-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.20.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

Versions declared directly in the project's dependencyManagement section take precedence over imported ones, regardless of their position. A version given in the dependencies section would override the BOM as well. Overrides should be rare and documented, since they break the consistency the BOM provides.

$ mvn dependency:tree
[INFO] com.example:override:jar:1.0.0
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.23.0:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.23:compile
[INFO] |  \- com.fasterxml.jackson.core:jackson-core:jar:2.23.0:compile
[INFO] \- org.apache.commons:commons-lang3:jar:3.20.0:compile
...

Several BOMs

A project can import several BOMs. If two of them manage the same artifact, the BOM imported first wins.

pom.xml (dependency management)
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson</groupId>
            <artifactId>jackson-bom</artifactId>
            <version>2.20.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>com.example</groupId>
            <artifactId>example-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

The project imports Jackson BOM 2.20.0 before our BOM, which brings Jackson 2.23.0. The first import wins, so all Jackson modules are at 2.20.0, while Commons Lang still comes from our BOM. This is the usual way to pin a library to a different version than a framework BOM, such as Spring Boot's, would choose: import the library's BOM before the framework's BOM.

$ mvn dependency:tree
[INFO] com.example:order:jar:1.0.0
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.20.0:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.20:compile
[INFO] |  \- com.fasterxml.jackson.core:jackson-core:jar:2.20.0:compile
[INFO] \- org.apache.commons:commons-lang3:jar:3.21.0:compile
...

A BOM in a multi-module project

In a multi-module project, the BOM is often one of the modules, and the parent POM imports it for all modules.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>parent-project</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <modules>
        <module>bom</module>
        <module>service</module>
    </modules>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>platform-bom</artifactId>
                <version>${project.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

The parent lists the bom and service modules and imports the BOM with ${project.version}, so the BOM and the modules are always released together. The BOM module is a copy of our earlier BOM named platform-bom; it must not use parent-project as its parent, because the parent imports it, which would create a cycle.

service/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>parent-project</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>service</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>
</project>

The service module inherits the imported BOM from its parent, so its dependencies need no versions.

service/src/main/java/com/example/Service.java
package com.example;

import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;

public class Service {

    record Status(String service, boolean healthy) {}

    public static void main(String[] args) {
        var status = new Status(StringUtils.upperCase("orders"), true);
        System.out.println(new Gson().toJson(status));
    }
}

A small program that uses both managed libraries.

Maven resolves the BOM from the reactor, the set of modules built together, so it does not have to be installed first. The -pl option selects a single module.

$ mvn package
[INFO] Reactor Summary for parent-project 1.0.0:
[INFO] platform-bom ....................................... SUCCESS [  0.006 s]
[INFO] parent-project ..................................... SUCCESS [  0.002 s]
[INFO] service ............................................ SUCCESS [  4.130 s]
[INFO] BUILD SUCCESS
...
$ mvn dependency:tree -pl service
[INFO] com.example:service:jar:1.0.0
[INFO] +- com.google.code.gson:gson:jar:2.15.0:compile
[INFO] |  \- com.google.errorprone:error_prone_annotations:jar:2.50.0:compile
[INFO] \- org.apache.commons:commons-lang3:jar:3.21.0:compile
...
$ mvn -q exec:java -pl service -Dexec.mainClass=com.example.Service
{"service":"ORDERS","healthy":true}

BOM or parent POM

The dependencyManagement section of a parent POM is inherited by its children, so a parent can play the role of a BOM. However, a project has only one parent, and the parent also passes on plugins, properties, and other build settings. A BOM affects only dependency versions, and a project can import any number of BOMs. Therefore, libraries publish BOMs for their users, while parent POMs are used to share build configuration within an organization.

Popular BOMs

Many libraries and frameworks publish a BOM. A few examples with recent versions:

BOMCoordinates
JUnitorg.junit:junit-bom:6.1.3
Jackson 2com.fasterxml.jackson:jackson-bom:2.23.0
Jackson 3tools.jackson:jackson-bom:3.3.0
Spring Bootorg.springframework.boot:spring-boot-dependencies:4.1.2

The Spring Boot BOM manages the versions of hundreds of libraries that are tested together with Spring Boot, including Jackson, JUnit, and Logback.

Best practices

Source

Bill of Materials (BOM) POMs

In this article we have shown how to create a Maven BOM and how to use it to manage dependency versions across projects and modules.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.