Maven dependencyManagement: Centralizing Versions Without Losing Sight of Them
Maven's dependencyManagement pins versions from a parent POM so multi-module builds stay aligned — but it only manages what you list, and over-centralizing can hide conflicts. Here's how to use it and verify it.
26 Sept 2025, 15:45 UTC

Your multi-module Maven build has twelve modules, and three of them declare Jackson. One says 2.15.2, one says 2.13.4, and one omits the version and gets whatever a transitive dependency drags in. Everything compiles, but at runtime you hit a NoSuchMethodError because the classpath settled on a version nobody actually chose. This is the exact problem dependencyManagement exists to solve — and it's also a feature you can overuse until debugging gets harder, not easier.
What dependencyManagement actually does
A <dependencyManagement> section in a POM declares defaults, not dependencies. It says: "if any module in this reactor declares artifact X, use this version (and optionally scope and exclusions) unless the module overrides it." Nothing is added to the classpath by the section itself. A module still has to declare the dependency in its own <dependencies> — it just gets to leave out the <version> tag.
In a multi-module build, the natural home for this is the parent POM. Every child module inherits the managed versions, so upgrading Jackson becomes a one-line change in one file instead of a grep-and-pray across a dozen POMs.
A worked example
Parent POM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<jackson.version>2.15.2</jackson.version>
<slf4j.version>2.0.7</slf4j.version>
</properties>Child module POM — no version needed:
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>Using properties for the version strings keeps the managed section readable and makes automated version bumps (via CI or tools like the Versions Maven Plugin) trivial to script.
Verifying the effective versions
Don't trust the POM text — trust what Maven resolves. From the project root, run:
mvn dependency:tree -Dincludes=com.fasterxml.jackson.coreThis needs no special permissions and changes nothing; it prints the resolved tree per module. You're checking two things: every module resolves the same Jackson version, and that version matches the parent POM. If a module shows something different, either it overrides the version locally or a transitive dependency is winning mediation — both worth investigating.
For a stronger guarantee, enable the Maven Enforcer Plugin's dependencyConvergence rule in the parent POM. It fails the build when two paths in the tree require different versions of the same artifact, catching conflicts that managed versions alone won't fix:
<rule>
<dependencyConvergence/>
</rule>Run mvn validate (or any later phase) after adding it. Expect it to fail the first time on a mature project — that's the point; it surfaces conflicts that were silently resolved by nearest-wins mediation.
The trade-off: centralization can hide the truth
Two real limitations deserve respect. First, dependencyManagement only pins what you list. Transitive dependencies — things your dependencies depend on — are unaffected unless you explicitly manage them too. A large managed section can create a false sense that "all versions are controlled" when most of the tree isn't.
Second, heavy centralization obscures provenance. When a module breaks after a parent POM bump, the failing version came from a file the module's developers may never look at. Mitigate this by keeping the managed list focused (shared libraries, not every artifact anyone uses), bumping versions in isolated commits, and letting CI run the full reactor before merging a parent change. A version bump is a state change to every module's classpath; treat it with the same care as a code change, including a rollback path — which, happily, is just reverting the commit.
A practical policy
Centralize versions for libraries that appear in more than one module or that must stay aligned as a set (the Jackson artifacts, Netty, anything with an internal compatibility matrix). Leave genuinely module-specific dependencies declared locally where readers can see them. Then close the loop: dependency:tree for spot checks, Enforcer's convergence rule for continuous enforcement. The goal isn't a perfectly tidy parent POM — it's a build where the version on the classpath is always a version someone deliberately chose.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.