ZetCode

Maven parent POM vs BOM

last modified November 13, 2026

In this article we compare two ways of sharing dependency versions between Maven projects: inheriting from a parent POM and importing a bill of materials (BOM). We build the same application with each of them, look at what each one shares, and combine both in the setup that most organizations use. The examples use Maven 3.10 and Java 25.

Both a parent POM and a BOM are POM files with pom packaging, and both usually contain a dependencyManagement section. They differ in how a project uses them. A project inherits from a parent: the parent's POM is merged into the project's POM. A project imports a BOM: only the dependencyManagement section of the BOM is copied into the project's own dependencyManagement. The Maven POM and Maven BOM articles cover the basics of both.

The parent POM

Our organization, ACME, uses two Apache Commons libraries. The first version of its shared configuration is a parent POM.

parent/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>acme-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <commons-lang3.version>3.21.0</commons-lang3.version>
        <commons-text.version>1.15.0</commons-text.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-text</artifactId>
                <version>${commons-text.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.17</version>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.5.1</version>
                    <configuration>
                        <archive>
                            <manifestEntries>
                                <Built-By>ACME</Built-By>
                            </manifestEntries>
                        </archive>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

The parent manages the versions of Commons Lang and Commons Text through properties, sets the Java release, adds the SLF4J API to every project, and configures the JAR plugin.

<properties>
    <maven.compiler.release>25</maven.compiler.release>
    <commons-lang3.version>3.21.0</commons-lang3.version>
    <commons-text.version>1.15.0</commons-text.version>
</properties>

Properties are inherited. A child project gets the Java release and can change a managed version by redefining the property.

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.17</version>
    </dependency>
</dependencies>

Dependencies in the dependencies section of a parent are inherited too. Every child gets SLF4J, whether it needs it or not.

<manifestEntries>
    <Built-By>ACME</Built-By>
</manifestEntries>

The build section, with plugin versions and configuration, is inherited as well. Every JAR built by a child carries the Built-By manifest entry.

Inheriting from the parent

An application refers to the parent with the parent element.

app-parent/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>acme-parent</artifactId>
        <version>1.0.0</version>
        <relativePath/>
    </parent>

    <artifactId>app-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>

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

The empty relativePath element tells Maven not to look for the parent in the parent directory; it is resolved from the repositories. We install the parent into the local repository with mvn install first. The Commons Text dependency has no version; it comes from the inherited dependency management.

app-parent/src/main/java/com/example/app/Main.java
package com.example.app;

import org.apache.commons.text.WordUtils;

public class Main {

    public static void main(String[] args) {
        System.out.println(WordUtils.capitalizeFully("an old falcon"));
    }
}

The application capitalizes words with Commons Text. The same class is used in all projects of this article.

The tree contains Commons Text 1.15.0, Commons Lang 3.21.0, which the parent manages for the transitive dependency, and SLF4J, which comes from the parent although the application does not use it. Redefining the commons-text.version property changes the managed version. On the command line we use -D; in a real project we would add the property to the properties section of the child POM. The manifest of the packaged JAR contains the Built-By entry from the inherited JAR plugin configuration.

$ mvn dependency:tree
com.example:app-parent:jar:1.0.0-SNAPSHOT
+- org.apache.commons:commons-text:jar:1.15.0:compile
|  \- org.apache.commons:commons-lang3:jar:3.21.0:compile
\- org.slf4j:slf4j-api:jar:2.0.17:compile
$ mvn dependency:tree -Dcommons-text.version=1.14.0
+- org.apache.commons:commons-text:jar:1.14.0:compile
$ unzip -p target/app-parent-1.0.0-SNAPSHOT.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 25
Build-Jdk-Spec: 25
Built-By: ACME

The BOM

The second version of the shared configuration is a BOM. It contains only the dependency versions.

bom/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>acme-bom</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <properties>
        <commons-lang3.version>3.21.0</commons-lang3.version>
        <commons-text.version>1.15.0</commons-text.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-text</artifactId>
                <version>${commons-text.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

The BOM manages the same two libraries with the same properties. It has no dependencies, no plugins, and no Java release; a BOM is meant to provide versions and nothing else.

Importing the BOM

The application imports the BOM in its dependencyManagement section.

app-bom/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-bom</artifactId>
    <version>1.0.0-SNAPSHOT</version>

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

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

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

The import needs the pom type and the import scope. The application is not tied to a parent, so it sets the Java release itself.

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

Maven reads the BOM, interpolates its properties in the context of the BOM, and copies the resulting managed dependencies into the project. Nothing else is copied.

The tree has the same Commons versions, but no SLF4J. Setting commons-text.version has no effect now, because the BOM's version expressions were resolved when it was imported; the property does not even exist in the project. To change a version that comes from a BOM, the project must manage the dependency itself, as shown below.

$ mvn dependency:tree
com.example:app-bom:jar:1.0.0-SNAPSHOT
\- org.apache.commons:commons-text:jar:1.15.0:compile
   \- org.apache.commons:commons-lang3:jar:3.21.0:compile
$ mvn dependency:tree -Dcommons-text.version=1.14.0
\- org.apache.commons:commons-text:jar:1.15.0:compile
$ mvn help:evaluate -Dexpression=commons-text.version -q -DforceStdout
null object or invalid expression

What is shared

The following table summarizes what a project gets from each of them.

POM sectionParent POMImported BOM
dependencyManagementinheritedimported
dependenciesinheritedignored
propertiesinherited, can be overriddenignored
build, pluginManagementinheritedignored
repositories, profilesinheritedignored
group ID and versioninheritedignored
number per projectoneany

The last row is the main practical difference. A project can have only one parent, but it can import as many BOMs as it needs. Projects that already have a parent, such as Spring Boot applications that inherit from spring-boot-starter-parent, can still take versions from a company BOM.

Combining both

In practice, the two are combined. The BOM manages dependency versions and can be used by anyone, including projects with a different parent. A platform parent POM imports the BOM and adds the build configuration: the Java release and the plugin versions.

platform/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>acme-platform-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

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

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

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>3.6.1</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

The parent has no managed dependencies of its own; it imports them from the BOM. Children inherit the imported entries together with the rest of the dependency management.

app/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>acme-platform-parent</artifactId>
        <version>1.0.0</version>
        <relativePath/>
    </parent>

    <artifactId>app</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.20.0</version>
            </dependency>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>6.1.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-text</artifactId>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

The application inherits from the platform parent, imports the JUnit BOM, and overrides one version.

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

A version declared directly in dependencyManagement wins over the versions imported from BOMs, wherever they come from. This is the way to change a single version that a BOM provides.

<dependency>
    <groupId>org.junit</groupId>
    <artifactId>junit-bom</artifactId>
    <version>6.1.3</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

The project imports a second BOM in addition to the one imported by the parent. When two BOMs manage the same artifact, the first import wins.

app/src/test/java/com/example/app/MainTest.java
package com.example.app;

import org.apache.commons.text.WordUtils;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class MainTest {

    @Test
    void capitalizes() {
        assertEquals("An Old Falcon", WordUtils.capitalizeFully("an old falcon"));
    }
}

A JUnit test checks the capitalization.

Commons Text comes from the ACME BOM through the parent, Commons Lang is the locally overridden 3.20.0, and the JUnit artifacts come from the JUnit BOM. Surefire runs in the version from the parent's plugin management.

$ mvn test dependency:tree
--- surefire:3.6.1:test (default-test) @ app ---
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: ... s -- in com.example.app.MainTest
com.example:app:jar:1.0.0-SNAPSHOT
+- org.apache.commons:commons-text:jar:1.15.0:compile
|  \- org.apache.commons:commons-lang3:jar:3.20.0:compile
\- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
   +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
   |  +- org.opentest4j:opentest4j:jar:1.3.0:test
   |  +- org.junit.platform:junit-platform-commons:jar:6.1.3:test
   |  +- org.apiguardian:apiguardian-api:jar:1.1.2:test
   |  \- org.jspecify:jspecify:jar:1.0.0:test
   +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test
   \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
      \- org.junit.platform:junit-platform-engine:jar:6.1.3:test
BUILD SUCCESS

When to use which

Source

Introduction to the dependency mechanism

In this article we have compared a parent POM with an imported BOM and combined both in a platform parent that imports a BOM.

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.