Managing Maven Transitive Dependency Versions Across Multi-Module Projects
Maven's dependencyManagement centralizes transitive dependency versions in multi-module builds. Learn how it works, see a worked example, and understand its limits.
24 Mar 2026, 01:58 UTC

The version drift problem
In a multi-module Maven build, the same library can reach different modules at different versions through transitive dependencies—dependencies pulled in indirectly by your direct dependencies. One module might resolve Guava 32.1.3-jre while another resolves 31.1-jre because an intermediate library changed its own requirements. The result can be split packages on the classpath, confusing NoSuchMethodError exceptions, or builds that pass locally but fail in CI.
The useful takeaway is that Maven's dependencyManagement section lets you pin those versions in the parent POM. Child modules inherit the decision, so you change a version once and every module follows—without forcing every module to declare the dependency directly.
How dependencyManagement works
dependencyManagement is a centralized version catalog. You declare groupId, artifactId, version, and optionally scope in the parent POM. When a child module lists a dependency with only groupId and artifactId, Maven resolves the version from the nearest dependencyManagement entry in the hierarchy. This behavior has been stable across Maven 3.x releases; the examples below assume a standard Maven 3.x layout.
A critical distinction: dependencyManagement does not add the dependency to any module's classpath. It only manages versions and scopes. You must still declare the artifact in a dependencies section where you actually need it. Think of it as a pricing list, not a shopping cart.
A worked example
Assume a parent POM and a child module named service-api. In the parent, define a managed version using a property:
<properties>
<guava.version>33.0.0-jre</guava.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
In the child module, declare Guava without a version:
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
To confirm the resolution, run the following from the child module directory (or from the project root with -pl service-api):
mvn dependency:tree
No special permissions are required beyond normal filesystem access. The expected check is that the tree output shows com.google.guava:guava:jar:33.0.0-jre (or whatever version your property defines). If a different version appears, either the child POM contains an explicit <version> tag, or a closer dependencyManagement entry is overriding the parent.
Now change guava.version in the parent to 33.1.0-jre and rebuild. The child module's effective version updates without editing its POM. That single-point upgrade is the main practical benefit.
Limitations and trade-offs
dependencyManagement is powerful but narrow. It does not resolve conflicts between two different transitive dependencies that both bring in different versions of a third library. Maven still applies its default mediation rules—typically "nearest wins"—which can produce a version you did not intend. For hard enforcement, the maven-enforcer-plugin with rules such as dependencyConvergence can fail the build when versions disagree.
Another risk is accidental override. If a developer adds an explicit <version> in a child module's dependencies section, that version takes precedence over the managed entry. This is sometimes necessary, but it silently reintroduces version drift. Treat explicit overrides as exceptions that require review.
Scope is also inherited. Changing a managed dependency's scope from compile to provided in the parent will affect every child that relies on the managed entry unless the child overrides it.
Verifying and maintaining the setup
Two commands give you confidence. mvn dependency:tree shows the actual resolved graph for a module. mvn help:effective-pom prints the merged POM, letting you confirm that dependencyManagement entries are inherited as expected. Run both from the module directory.
If a centralized version breaks a specific module, the rollback is straightforward: revert the parent POM change, or temporarily add an explicit version in the affected child module with a comment explaining why. The latter is a local escape hatch, not a long-term pattern.
Start by auditing your parent POM for repeated version strings, move them into dependencyManagement backed by properties, and verify one module with dependency:tree before rolling the pattern out. The payoff is a build where version decisions live in one place and upgrades are a single-line change.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.