Maven dependencyManagement, BOMs, Version Alignment, Enforcer, and Dependency Analysis: Concepts, Architecture, and Mental Model
Separate Maven dependency declarations from version policy, then connect imported BOMs, dependency convergence rules, upper-bound checks, and bytecode-based dependency analysis to the resolved dependency graph.
Chapter 06 made dependency edges and classpaths visible; Chapters 07–10 made plugin, environment, reactor, and test behavior explicit. The next scaling problem is policy: when twenty modules need the same libraries, where should versions live, and how can CI prove that the resolved graph still obeys the policy?
Learning objectives
- Distinguish a dependency declaration from a dependencyManagement policy entry.
- Explain how an imported BOM contributes managed coordinates without adding classpath edges.
- Compare dependencyConvergence and requireUpperBoundDeps as different graph policies.
- Explain what dependency:analyze can and cannot prove from bytecode.
- Inspect effective dependency policy before changing the build.
~/.m2,
global settings, production CI, or real artifact repository is
modified.
1. The practical problem: policy is not the graph
In a small project, repeating a version in every
<dependency> can look harmless. In a large
repository it creates drift: one module updates, another does not, a
third receives a different transitive version, and the build remains
green only because Maven mediation happens to select something
compatible.
Maven therefore separates two concerns. Declaration says “this project needs this dependency,” creating a graph edge. Management says “if this coordinate appears, this is the version/scope/exclusion policy to use.” Management is a policy table; it does not itself add the library to the compile or runtime classpath.
2. Mental model: declaration, management, import, resolution, enforcement
flowchart TD P[Project POM] --> D[dependencies: graph edges] P --> M[dependencyManagement: policy] B[BOM POM] -->|type=pom + scope=import| M D --> R[Resolved dependency graph] M --> R R --> C[Compile/runtime classpaths] R --> E[Enforcer policy checks] C --> A[dependency:analyze bytecode evidence]
The arrows are deliberately different. A declaration enters resolution as an edge. A BOM import enters the management table. Maven combines them when it resolves versions. Enforcer evaluates graph/model rules during the build. The Dependency Plugin can then compare compiled bytecode usage with declared dependencies, but that analysis is not equivalent to runtime tracing.
3. dependencyManagement is a policy table, not an include list
A managed entry is matched by dependency identity (normally
groupId:artifactId for an ordinary JAR without
classifier). It can supply a version when a child declaration omits
one and can control versions encountered transitively. Maven
documentation is explicit that dependency management takes
precedence over ordinary transitive mediation for project
dependencies.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
</dependencyManagement>
This fragment alone does not place Commons Lang on any classpath. A child still needs a dependency edge, directly or transitively. That distinction is one of the most important Maven governance invariants.
4. BOM import: reusable management without parent inheritance
A Bill of Materials (BOM) is normally a POM-packaged artifact whose
dependencyManagement defines a coordinated family of
versions. A consumer imports that managed set by declaring the BOM
inside its own dependencyManagement with
<type>pom</type> and
<scope>import</scope>.
<dependency>
<groupId>dev.academy.governance</groupId>
<artifactId>academy-platform-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
bom packaging for the newer 4.1.0 model. This course
remains on the Maven 3.9.16 production path, so the mandatory labs
use the long-established pom packaging plus
type=pom/scope=import.
5. Precedence: local policy, parents, and multiple imports
Policy can arrive from the current POM, its parent, and imported BOMs. A declaration in the current POM's dependencyManagement can override inherited management. When multiple imported BOMs manage the same coordinate and the consuming POM does not manage it directly, Maven's documented example shows that the BOM imported first can determine the version. That is why import order is reviewable policy, not formatting noise.
| Situation | What to inspect | Why |
|---|---|---|
| Child omits version | Effective POM + dependency tree | Proves which management entry supplied the selected version. |
| Current POM manages same coordinate as parent | Current dependencyManagement | Current POM management takes precedence. |
| Two BOMs manage same coordinate | Import order + effective POM | A duplicate managed coordinate can resolve according to import precedence. |
| Plugin dependency differs | Plugin configuration/dependency graph | Project dependencyManagement does not universally govern plugin transitive dependencies. |
6. Enforcer: convergence and upper-bound policies answer different questions
dependencyConvergence asks whether the same dependency appears at different versions anywhere in the graph paths covered by the rule. A mediated build can therefore run with one selected version and still fail convergence because conflicting requests exist in the graph.
requireUpperBoundDeps is narrower: it checks that the resolved version is at least the highest version requested by the dependency paths, subject to the rule's comparison semantics. It can tolerate some non-convergent graphs that still select the highest requested version. Neither rule “fixes” dependencies; each rejects a graph that violates its policy.
| Policy | Question | Typical use |
|---|---|---|
| dependencyConvergence | Are all paths asking for the same version? | Strict graph consistency and explainability. |
| requireUpperBoundDeps | Did resolution choose a version lower than another requested version? | Prevent accidental downgrades caused by nearest-wins mediation. |
| No Enforcer graph rule | Can Maven resolve something? | Too weak as a production governance criterion by itself. |
7. Dependency analysis is bytecode evidence, not runtime omniscience
dependency:analyze classifies dependencies as
used-and-declared, used-but-undeclared, or unused-and-declared. In
3.11.0 the default analyzer works at bytecode level. That is
valuable, but reflection, service loading, generated code, framework
discovery, native integrations, and resource-based lookups can
escape ordinary static references.
The plugin therefore exposes targeted mechanisms such as
usedDependencies to document a known dependency as used
when bytecode analysis cannot see the relationship. A false positive
should produce an explicit, reviewed exception—not a habit of
disabling analysis.
8. Read-only inspection before mutation
./mvnw, grep, find, and
sha256sum. On Windows use mvnw.cmd,
Select-String, Get-ChildItem, and
Get-FileHash. Maven dependency-management and Enforcer
semantics are cross-platform; shell quoting and path syntax are not.
set -euo pipefail
./mvnw -v
java -version
mkdir -p evidence
./mvnw -Dmaven.repo.local=.lab-m2 help:effective-pom -Dverbose > evidence/effective-pom.xml
./mvnw -Dmaven.repo.local=.lab-m2 org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree > evidence/dependency-tree.txt
./mvnw -Dmaven.repo.local=.lab-m2 org.apache.maven.plugins:maven-dependency-plugin:3.11.0:analyze > evidence/dependency-analysis.txt
help:effective-pom answers “what
management/configuration is active?” The tree answers “what graph
was resolved?” Analysis answers “what compiled bytecode appears to
use?” Keep those evidence types separate.
9. DevOps and supply-chain boundary
Central version policy reduces drift, but it also centralizes power. A BOM update can change hundreds of classpaths; an Enforcer exception can weaken a gate across many modules; and a parent POM can make policy invisible to a casual reader. Treat parent/BOM changes like executable delivery-policy changes: pin coordinates, review diffs, run clean-room resolution, retain tree evidence, and avoid dynamic versions as production defaults.
Knowledge check
Does dependencyManagement add a library to a child classpath?
No. It supplies policy when a dependency is actually declared or encountered transitively; it does not create the dependency edge itself.
What two elements make a Maven 3 BOM import an import rather than an ordinary POM dependency?
type=pom and scope=import, inside dependencyManagement.
Why can dependencyConvergence fail even when Maven selected exactly one JAR version?
Because the rule checks whether graph paths request conflicting versions, not merely which version mediation selected.
Why can dependency:analyze report a real runtime dependency as unused?
The default analyzer reasons from bytecode; reflection or framework/resource discovery can use a dependency without a direct static bytecode reference.
Does a BOM constitute a universal Maven lockfile?
No. It centralizes managed versions for coordinates it covers; it does not by itself freeze every transitive dependency, plugin, repository response, or artifact byte.
10. Summary and bridge
Dependency declarations create graph edges; dependencyManagement and imported BOMs supply version policy. Enforcer makes chosen graph invariants executable, while dependency analysis checks declaration quality from compiled evidence with known limits. Lesson 2 turns those distinctions into a small local platform BOM and two governed modules.
Official references and version notes
Version-sensitive statements were checked against Apache Maven primary documentation on 2026-08-23. The mandatory path pins Maven 3.9.16 via Maven Wrapper 3.3.4, JDK 21 to run Maven, Java 17 as the compiler release target, Maven Dependency Plugin 3.11.0, Maven Enforcer Plugin 3.6.3, Help Plugin 3.5.2, and Compiler Plugin 3.15.0.
The illustrative dependency family deliberately uses
org.apache.commons:commons-text:1.10.0 and
org.apache.commons:commons-lang3:3.12.0 because it
produces a small, stable graph for explaining management and
convergence. These are teaching pins, not claims that the versions
are the newest releases.
- Maven — Introduction to the Dependency Mechanism
- Maven Dependency Plugin 3.11.0 — Introduction
- Maven Dependency Plugin 3.11.0 — dependency:analyze
- Maven Dependency Plugin 3.11.0 — dependency:analyze-only
- Maven Enforcer Plugin 3.6.3 — Introduction
- Enforcer Rule — dependencyConvergence
- Enforcer Rule — requireUpperBoundDeps
- Maven Help Plugin 3.5.2
- Maven Compiler Plugin 3.15.0
- Apache Maven Wrapper
- Maven 3.9.16 Release Notes
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.