Solving Version Drift in Multi-Module Maven Projects with Dependency Management
Stop fighting 'jar hell' and version drift. Learn how to use Maven's <dependencyManagement> and the BOM pattern to centralize version control in multi-module projects.
19 Jan 2026, 17:25 UTC

The Problem: The 'Hidden' Version Conflict
In a multi-module Maven project, it is common for different modules to rely on the same library but transitively pull in different versions. For example, Module A might depend on Library X (v1.2), while Module B depends on Library Y, which itself requires Library X (v1.5). Because Maven uses a "nearest-definition" resolution strategy, the version that ends up on your classpath depends entirely on the order of your dependency tree.
This leads to "jar hell": runtime NoSuchMethodError or ClassNotFoundException errors that only appear after the application is packaged, despite the code compiling perfectly in individual modules. The solution is to decouple version declaration from dependency inclusion using the <dependencyManagement> section.
Centralizing Control with Dependency Management
The <dependencyManagement> block acts as a lookup table for Maven. It does not actually add a JAR to the classpath; instead, it tells Maven: "If any module in this project (or its children) decides to use this artifact, use this specific version."
By placing this block in a parent POM, you ensure that every child module uses the exact same version of a library, regardless of what a transitive dependency might request. This eliminates version drift across your microservices or modules and simplifies upgrades—you change the version in one place, and the entire project aligns.
The Bill of Materials (BOM) Pattern
When you need to share a consistent set of versions across entirely separate projects (where a parent-child inheritance relationship isn't possible), you use a Bill of Materials (BOM). A BOM is a POM file that contains only a <dependencyManagement> section.
You can pull this curated list into any project using the import scope. This is how frameworks like Spring Boot manage hundreds of compatible library versions without requiring you to manually specify a version for every single starter dependency.
Worked Example: Implementing a Parent BOM
Assume a multi-module project with a parent POM and two children: api-service and worker-service. We want to ensure both use Jackson 2.15.2.
1. The Parent POM
Run this in the root directory. Note that the version is defined here, but the library is not yet "active" in any module.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>project-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2. The Child POM
In the child module, you declare the dependency without a version tag. Maven will look up the version from the parent's management block.
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<!-- No version specified here --
</dependency>
</dependencies>
3. Verification
To verify that the version was correctly inherited, run the following command from the child module directory (requires Maven installed and configured in your shell):
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core
Expected Result: The output should explicitly show jackson-databind:jar:2.15.2. If the version is missing or different, check that the child POM correctly references the parent POM in its <parent> block.
Trade-offs and Limitations
While powerful, <dependencyManagement> has specific constraints:
- Explicit Declaration Required: A common mistake is assuming that adding a library to
<dependencyManagement>automatically adds it to the classpath. It does not. You must still add the dependency to the<dependencies>block of the child module. - BOM Conflicts: If you import two different BOMs (e.g., a cloud provider BOM and a framework BOM) that both define the same artifact with different versions, the one declared first in the
<dependencyManagement>section wins. - Tight Coupling: Over-stuffing a single parent POM with every possible dependency can create a "monolithic POM," making it difficult to split modules into separate projects later.
Actionable Summary
To stop chasing transient version bugs, shift your strategy: define all shared library versions in a parent <dependencyManagement> block or import a trusted BOM. In your child modules, remove all version tags from dependencies that are managed. This creates a single source of truth and ensures your build is deterministic across all environments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.