Choosing a Dependency Version Strategy for Maven Multi-Module Projects
Compare Maven BOM import, parent POM dependencyManagement, and version properties for multi-module builds, with trade-offs and validation commands.
16 Jul 2026, 07:55 UTC

The Decision: Three Ways to Pin a Version
In a Maven multi-module reactor, version drift happens when different modules resolve different versions of the same artifact. One service compiles against 1.2.3 while another pulls 1.4.0, and the mismatch only surfaces in integration testing or production. The useful takeaway is that Maven offers three supported mechanisms for centralizing versions—BOM import, parent POM dependencyManagement, and version properties—and the right choice depends on whether your modules share a parent, whether you consume third-party ecosystems, and how much drift you can tolerate.
Constraints to settle first
- Inheritance: Must every module share the same parent? If not, BOM import is the only mechanism that works across arbitrary POMs.
- Ecosystem: Are you consuming Spring Boot, Quarkus, or Jakarta EE? Their BOMs are designed for import, not inheritance.
- Enforcement: Can you fail the build on conflicts? Without the Enforcer plugin, properties and overlapping BOMs allow silent drift.
Comparing the Supported Options
| Approach | Inheritance required | Ecosystem alignment | Transitive enforcement | Best for |
|---|---|---|---|---|
BOM import (<scope>import</scope>) | No | High | Yes, via imported BOM | Consuming third-party BOMs; mixing version sources without a common parent |
Parent POM dependencyManagement | Yes | Medium | Yes, within the reactor | Single-team monorepos with tight internal control |
| Version properties | Optional | Low | No | Ad-hoc overrides, legacy migrations, quick experiments |
Trade-offs in Practice
BOM import: composition without inheritance
A BOM is a POM-only artifact that contains dependencyManagement entries. Importing it with <scope>import</scope> injects those managed versions into the importing POM without requiring a parent-child relationship. This is how Spring Boot and Quarkus distribute curated version sets. Some ecosystem BOMs, such as Spring Boot, import transitive dependencyManagement from their own parent; combining them with another BOM that manages the same artifacts may require explicit ordering or overrides. The trade-off is that importing multiple BOMs with overlapping artifacts leaves version selection order undefined unless you add explicit dependencyManagement entries or enforce upper bounds.
Parent POM dependencyManagement: reactor-bound control
Placing dependencyManagement in a parent POM binds version governance to the reactor. Every module must declare that parent. This is simple for a single-team repository, but it prevents modules from using third-party BOMs alongside the parent's managed versions without extra configuration, and it does not help projects that cannot inherit the parent.
Version properties: flexible but unenforced
Properties such as <jackson.version>2.15.2</jackson.version> are easy to override per module. However, they are not visible to dependency:tree unless explicitly referenced, and they bypass Maven's built-in conflict detection. Without tooling like the Versions plugin or Enforcer rules, properties invite drift.
Concrete Implementation and Validation
The following pattern combines a parent POM with an internal BOM so that a child module inherits the parent but consumes versions from the BOM. Run all commands from the root reactor directory unless noted; you need read access to the project files and local Maven repository, plus network access if artifacts are not cached.
Parent POM with BOM import
<project>
<groupId>com.example</groupId>
<artifactId>platform-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<example-bom.version>1.0.0</example-bom.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>example-bom</artifactId>
<version>${example-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Child module inheriting the parent
<project>
<parent>
<groupId>com.example</groupId>
<artifactId>platform-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>service-a</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<!-- version omitted; managed by example-bom -->
</dependency>
</dependencies>
</project>
Validating resolved versions
To confirm that the child module resolves the intended version, inspect the effective POM and dependency tree:
mvn help:effective-pom -pl service-a— shows the mergeddependencyManagementafter BOM import and inheritance.mvn dependency:tree -Dincludes=org.apache.commons:commons-lang3— prints the resolved version for that artifact across the reactor.mvn versions:display-dependency-updates— lists available newer versions so you can spot drift between the BOM and the latest release.
Enforcing deterministic resolution
If you import more than one BOM, or if modules may declare lower versions than the BOM, add the Enforcer plugin to the parent POM:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${maven.enforcer.version}</version>
<executions>
<execution>
<id>enforce-bounds</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireUpperBoundDeps/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Set maven.enforcer.version to a release available in your artifact repository. Run mvn enforcer:enforce from the root. The requireUpperBoundDeps rule fails the build when a direct or transitive dependency declares a version lower than the one managed by the BOM. If the rule rejects a dependency you intentionally need, remove the <execution> block from the parent POM to restore the previous build behavior.
Limitations and Version-Specific Behavior
- Overlapping BOMs: Maven does not define a deterministic selection order when two imported BOMs manage the same artifact. Explicit
dependencyManagemententries or the Enforcer plugin are required for predictable results. - Maven version differences: Maven 3.9+ provides clearer BOM import conflict diagnostics. Maven 4.0 (preview) introduces consumer-side dependency resolution that may alter BOM precedence. Verify behavior on the Maven version your CI uses; pin the version with the Maven Wrapper (
mvnw) to avoid local-versus-pipeline drift. - Properties are invisible to conflict detection: A property-referenced version does not participate in Maven's nearest-wins conflict resolution unless the property is used in a
dependencyManagemententry. - Parent scope limitation:
dependencyManagementin a parent POM has no effect on projects that do not declare that parent.
Practical Result Check
After applying the pattern, run mvn dependency:tree -Dverbose from the root and confirm that every module resolves the same version for shared artifacts. Then run mvn enforcer:enforce to ensure no module can silently downgrade a managed dependency. If both checks pass, your version strategy is aligned with the reactor.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.