Taming Dependency Version Drift with Maven BOMs
Maven BOMs centralize dependency versions across multi-module projects without pulling transitive JARs. This post covers import mechanics, reactor propagation, override patterns, and the limits you'll hit at scale — plus a minimal worked example you can run today.
20 Sept 2026, 19:52 UTC

The Version Drift Problem
You have twelve microservices, each declaring jackson-databind with its own version. Six months later, a CVE drops. You discover three services still pull 2.13.4 while the rest upgraded to 2.15.2. The root cause isn't negligence — it's that every pom.xml owns its version numbers, and nothing forces alignment.
Maven's Bill of Materials (BOM) feature exists precisely for this: a single source of truth for dependency versions that propagates across modules without pulling transitive JARs. This post walks through how BOM import works, where it shines in multi-module reactors, and the trade-offs you'll hit when scaling it.
How BOM Import Actually Works
A BOM is just a POM with <packaging>pom</packaging> that declares versions inside <dependencyManagement>. Consumers import it by adding the same coordinates with <scope>import</scope> inside their own <dependencyManagement> block. Crucially, import scope is only valid inside dependencyManagement — putting it in <dependencies> fails the build with "import scope not allowed here."
When Maven resolves the consumer's effective POM, the imported BOM's dependencyManagement entries are merged in. The consumer then declares dependencies without versions (e.g., <artifactId>jackson-databind</artifactId>), and Maven fills in the version from the BOM. No JARs are downloaded from the BOM itself — it contributes only metadata.
Propagating Alignment in a Multi-Module Reactor
In a typical multi-module project, place the BOM import in the root POM's dependencyManagement. Every child module inherits that merged version map automatically. This eliminates the "each service picks its own version" drift because there's literally one place to update.
Example root pom.xml snippet:
<project>
<packaging>pom</packaging>
<modules>
<module>service-a</module>
<module>service-b</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.myorg</groupId>
<artifactId>platform-bom</artifactId>
<version>2024.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Run mvn help:effective-pom -pl service-a from the reactor root to verify the merged dependencyManagement shows the BOM's versions. If you see your managed dependencies listed without explicit versions in the child's effective POM, propagation works.
Overriding a Single Version Without Forking
Sometimes you need a security patch for one library before the upstream BOM releases it. Maven's nearest-wins rule in dependencyManagement lets you override after the import:
<dependencyManagement>
<dependencies>
<!-- import first -->
<dependency>
<groupId>com.myorg</groupId>
<artifactId>platform-bom</artifactId>
<version>2024.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- override second -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
</dependency>
</dependencies>
</dependencyManagement>
The override wins because it appears later in the same dependencyManagement list. Verify with mvn help:effective-pom — the effective entry for jackson-databind should show 2.15.3, not the BOM's version.
Worked Example: Create and Consume a Minimal BOM
Run these commands from a clean workspace. You need Maven 3.9+ and write access to your local repository (~/.m2/repository).
- Generate the BOM project:
mvn archetype:generate -DartifactId=my-bom -Dpackaging=pom -DinteractiveMode=false - Edit
my-bom/pom.xmland add adependencyManagementblock with one entry, e.g.,org.slf4j:slf4j-api:2.0.9. - Install the BOM locally:
cd my-bom && mvn install(requires network for plugin downloads on first run). - Create a consumer module:
mvn archetype:generate -DartifactId=consumer -Dpackaging=jar -DinteractiveMode=false(in a sibling directory). - Add the import to
consumer/pom.xml: inside<dependencyManagement>, add the dependency with<scope>import</scope>and<type>pom</type>pointing tocom.example:my-bom:1.0-SNAPSHOT(adjust groupId to match your BOM). - Declare the managed dependency without version: in
<dependencies>, addorg.slf4j:slf4j-apiwith no<version>tag. - Verify resolution: run
mvn dependency:treefrom the consumer directory. You should seeslf4j-api:jar:2.0.9:compile— the version came from the BOM.
Risk note: If you change the BOM's <packaging> to jar and rebuild, Maven 3.9+ emits a warning "Importing a non-pom project is deprecated" and ignores the import. Older versions silently ignore it, leaving your consumer unresolved. Always keep packaging=pom.
Trade-offs and Limitations
- Plugin versions are not managed. BOMs only affect
dependencyManagement. Plugin versions live inpluginManagement— either maintain a separate parent POM or use a tool likeversions-maven-pluginto keep them in sync. - Transitive mediation still applies. If two BOM-imported dependencies pull different versions of a third-party library (e.g.,
guava), Maven's nearest-wins rule decides, which may not match the BOM's intent. Usemvn dependency:tree -Dverboseto spot conflicts. - Too many BOM imports slow resolution. Importing 10+ platform BOMs (Spring Boot, Quarkus, MicroProfile, etc.) adds overhead and obscures ownership. Prefer a single curated organizational BOM that re-exports selected platform BOMs in a defined order.
- Snapshot BOMs break reproducibility. A
-SNAPSHOTversion resolves to whatever is latest in the repository at build time. For release pipelines, lock to a timestamped build (1.0.0-20240115.123456-1) or promote to a proper release version.
Actionable Next Steps
- Audit your reactor: run
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databindacross modules and count distinct versions. - If you see drift, create an organizational BOM project (packaging=pom) and declare the versions you want standardized.
- Import that BOM in your root POM's
dependencyManagementwithscope=import. - Remove explicit
<version>tags from child modules for those dependencies. - Add
versions-maven-pluginto your CI to detect when managed dependencies have updates (mvn versions:display-dependency-updates). - Mirror the BOM (and its transitive dependencies) to your internal repository manager for air-gapped builds.
Start with one high-churn library (Jackson, Netty, Spring) and expand. The goal isn't perfection — it's making the next CVE response a single-version bump in one file instead of a week-long hunt across repositories.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.