Centralizing Dependency Versions with Maven dependencyManagement in Multi-Module Builds
Pin dependency versions once in a parent POM with dependencyManagement. Let child modules inherit them, and verify the result with dependency:tree — without accidentally bypassing your own rules.
04 May 2026, 16:32 UTC

The problem: version drift across modules
In a multi‑module Maven project, each module declares its own dependencies. If three modules each declare JUnit with a hard‑coded version, upgrading means editing three POMs — and forgetting one leaves modules testing against different versions of the same library. The same drift happens with logging frameworks, JSON parsers, and anything else shared across modules.
The useful takeaway: <dependencyManagement> in a parent POM lets you pin each dependency's version in exactly one place. Child modules declare the dependency without a version and inherit the managed one. Changing the version in the parent updates every module on the next build. This behavior has been stable since Maven 2.0 and works the same in Maven 3.x.
What dependencyManagement does — and does not do
<dependencyManagement> is a lookup table, not a dependency declaration. It records: when a module asks for artifact X, use version Y. Key consequences:
- It does not put anything on the classpath. A module must still declare the dependency in its own
<dependencies>section — just without the<version>element. - If a module never declares the dependency, it is never resolved, even though it appears in the parent's management section.
- If a child declares an explicit version, that explicit version wins over the managed one. This is the escape hatch, but it also means an accidental hard‑coded version silently bypasses your central control.
- It also constrains transitive dependencies: if module code pulls in library A which depends on library B, and B is listed in your
<dependencyManagement>, Maven resolves B to your managed version rather than the one A requested.
Prerequisites
- Maven 3.6+ installed (
mvn -versionto check). - A multi‑module project: a parent POM with
<packaging>pom</packaging>and a<modules>list, plus at least one child module whose<parent>points at it. - Write access to the parent POM. Only the parent (or an imported BOM) should carry the management section for shared libraries.
Setting it up
In the parent POM, add a <dependencyManagement> section. Example pinning JUnit and SLF4J:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
</dependencyManagement>
Note that scope is managed too: the child inherits test scope for JUnit unless it overrides it.
In each child module, declare the dependency with no version:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
Run commands from the project root (where the parent POM lives) so the reactor builds all modules. No special permissions are needed beyond normal repository read access.
Verifying the managed version is applied
From the project root, run:
mvn dependency:tree -pl module-name
The -pl flag limits the build to one module. In the output tree, confirm JUnit appears with version 4.13.2 even though the child POM declares no version. If the version differs from what the parent pins, look for an explicit <version> in the child POM or a conflicting entry in another parent's management section.
To confirm the central‑upgrade workflow, change the JUnit version in the parent to a different release and run mvn clean install from the root. All modules should resolve the new version without any child POM edits. Re‑run dependency:tree on one module to check.
To confirm the classpath limitation, temporarily remove the JUnit <dependency> block from a child module while leaving the parent's management entry in place, then run mvn test for that module. Test compilation should fail with missing JUnit classes — proving that management alone adds nothing.
Overriding deliberately, and the risk of doing it accidentally
A child can pin a different version by declaring one explicitly. Do this only with a comment explaining why, because it defeats the point of centralization:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version> <!-- legacy: module X not yet compatible with 2.x -->
</dependency>
The bigger risk is accidental overrides: a developer copies a dependency snippet from documentation that includes a version, and that module quietly stops following the managed version. A periodic audit helps — run mvn dependency:tree per module and diff the resolved versions of your managed artifacts, or enforce the rule with the Maven Enforcer plugin.
Limitations
- Management is per‑artifact; it does not align related artifacts automatically. For frameworks with many coordinated modules (Spring, Jackson), prefer importing their BOM with
<scope>import</scope>inside<dependencyManagement>rather than listing each artifact yourself. - It manages versions, scopes, and exclusions — not whether a dependency exists. Modules still need explicit declarations.
- Plugin versions are controlled separately by
<pluginManagement>; the two sections are independent.
Recovery options
These changes only edit POM files, so recovery is version control: git checkout -- pom.xml restores the parent. If a bad managed version reached your local repository, Maven will re‑resolve correctly once the POM is fixed; use mvn dependency:purge-local-repository -DmanualInclude=groupId:artifactId only if you suspect a corrupted artifact, since purging forces re‑downloads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.