Maven dependencyManagement, BOMs, Version Alignment, Enforcer, and Dependency Analysis: Guided Hands-On Workflow and Core Operations
Build a disposable Maven governance lab with a small BOM, two consumer modules, versionless declarations, Enforcer checks, effective-model inspection, dependency trees, and dependency analysis evidence.
The mental model becomes useful only when you can prove each layer separately. This workflow installs a tiny BOM into an isolated local repository, imports it into a parent/aggregator, consumes versionless dependencies from two modules, and captures model, graph, Enforcer, and analysis evidence.
Learning objectives
- Create and install a Maven 3-compatible BOM into disposable local state.
- Import that BOM and consume managed dependencies without child versions.
- Run dependencyConvergence and dependency analysis as explicit gates.
- Inspect effective dependency management and resolved trees.
- Use before/after evidence to explain every version decision.
~/.m2,
global settings, production CI, or real artifact repository is
modified.
1. Disposable workspace and preflight
./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
mkdir -p maven-governance-lab/{platform-bom,apps/app-a/src/main/java/dev/academy/governance,apps/app-b/src/main/java/dev/academy/governance,evidence}
cd maven-governance-lab
./mvnw -v
java -version
printf 'Local repository: %s\n' "$PWD/.lab-m2"
find . -maxdepth 3 -type f -print | sort > evidence/preexisting-files.txt
.lab-m2 is
disposable lab state, not your normal Maven repository.
2. Create the platform BOM
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.academy.governance</groupId>
<artifactId>academy-platform-bom</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<commons-text.version>1.10.0</commons-text.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
The BOM has no application code and no
<dependencies> section. Its job is policy
publication: two coordinates, two pinned versions.
packaging=pom makes it a Maven 3-compatible BOM
artifact.
set -euo pipefail
./mvnw -Dmaven.repo.local=.lab-m2 -f platform-bom/pom.xml install | tee evidence/bom-install.log
find .lab-m2/dev/academy/governance/academy-platform-bom/1.0.0 -maxdepth 1 -type f -print | sort | tee evidence/bom-local-files.txt
Expected state: the isolated repository contains the BOM POM and Maven metadata. No JAR is expected because the project packages a POM.
3. Import the BOM in the applications parent
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.academy.governance</groupId>
<artifactId>governed-apps</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>app-a</module>
<module>app-b</module>
</modules>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.academy.governance</groupId>
<artifactId>academy-platform-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.3</version>
<executions>
<execution>
<id>dependency-policy</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules><dependencyConvergence/></rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.11.0</version>
<executions>
<execution>
<id>dependency-analysis</id>
<goals><goal>analyze-only</goal></goals>
<configuration><failOnWarning>true</failOnWarning></configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
This POM has three independent responsibilities: aggregate the two application modules, import version policy from the installed BOM, and activate build-quality plugins. The imported BOM does not create dependencies in either child.
4. Child modules declare what they actually use
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.academy.governance</groupId>
<artifactId>governed-apps</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>app-a</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>
package dev.academy.governance;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
public final class AppA {
public static String render(String input) {
String normalized = StringUtils.defaultIfBlank(input, "empty");
return StringEscapeUtils.escapeHtml4(normalized);
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.academy.governance</groupId>
<artifactId>governed-apps</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>app-b</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<configuration>
<usedDependencies>
<usedDependency>org.apache.commons:commons-lang3</usedDependency>
</usedDependencies>
</configuration>
</plugin>
</plugins>
</build>
</project>
package dev.academy.governance;
import java.lang.reflect.Method;
public final class AppB {
public static boolean reflectiveBlankCheck(String input) throws Exception {
Class<?> type = Class.forName("org.apache.commons.lang3.StringUtils");
Method method = type.getMethod("isBlank", CharSequence.class);
return (Boolean) method.invoke(null, input);
}
}
Neither child writes a dependency version. The imported BOM supplies those values. App A has ordinary static references to both libraries. App B intentionally loads Commons Lang by reflection; its local Dependency Plugin configuration documents that bytecode analysis cannot see this runtime relationship.
5. Predict before building
Write these predictions into
evidence/predictions.txt before running Maven:
| Prediction | Expected result | Evidence |
|---|---|---|
| Managed is not declared | Only coordinates under child dependencies become direct graph edges. | Effective POM versus dependency tree. |
| BOM alignment | app-a selects commons-text 1.10.0 and commons-lang3 3.12.0. | dependency:tree. |
| Convergence | Commons Text transitively requests Commons Lang 3.12.0, matching the direct managed version. | Enforcer output + tree. |
| Reflection exception | app-b keeps Commons Lang even if bytecode analysis would otherwise report it unused. | analyze-only output + documented usedDependencies. |
6. Inspect effective management and resolved graph
set -euo pipefail
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/app-a/pom.xml help:effective-pom -Dverbose > evidence/app-a-effective-pom.xml
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/app-a/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree > evidence/app-a-tree.txt
grep -n "commons-text\|commons-lang3\|academy-platform-bom" evidence/app-a-effective-pom.xml evidence/app-a-tree.txt || true
The effective POM should show versions materialized into the model even though the child source POM omits them. The dependency tree should show actual graph edges. Do not infer declaration from effective management alone.
7. Run the governed build and analyze declarations
set -euo pipefail
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/pom.xml clean verify | tee evidence/governed-verify.log
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/app-a/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.11.0:analyze | tee evidence/app-a-analyze.log
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/app-b/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.11.0:analyze | tee evidence/app-b-analyze.log
verify reaches the Enforcer execution and
analyze-only execution declared in the parent. Because
App B documents its reflective dependency through
usedDependencies, the quality gate can remain strict
instead of globally ignoring unused-dependency warnings.
8. Challenge: choose the correct control
A third module needs Commons Lang directly, but its developer copies
<version>3.12.0</version> into the child.
The build passes today. Which is the better correction?
Remove the duplicated child version and let the imported BOM supply it. An exclusion would remove an edge, Enforcer would only reject a bad graph, and another parent property would duplicate the platform policy. The correct control is the existing BOM-managed coordinate.
9. Verification and cleanup
Before cleanup, keep evidence/ long enough to compare
the effective model, tree, Enforcer output, and analysis
classifications. Then remove only the disposable lab workspace.
Never delete normal user Maven state to “prove” freshness.
cd ..
rm -rf maven-governance-lab
Knowledge check
Why install the BOM before building the separate apps reactor?
The consumers import the BOM as a model artifact. Installing it into the same isolated repository makes that model resolvable without relying on a remote service.
Why can the child omit a dependency version?
Its effective dependencyManagement contains the imported BOM entry for that coordinate.
What proves that a BOM entry is active but not automatically declared?
Compare the effective POM management section with the actual dependency tree/direct child dependencies.
Why use usedDependencies for App B instead of disabling dependency analysis?
It documents one known bytecode-analysis blind spot while keeping the broader declaration-quality gate active.
What state is safe to delete at the end?
Only the disposable lab workspace, including .lab-m2; not the learner’s normal ~/.m2 repository.
10. Summary and bridge
You now have a reproducible local governance path: publish a BOM into controlled state, import it, omit child versions intentionally, inspect effective management, verify the resolved graph, enforce convergence, and analyze declaration quality. Lesson 3 turns those mechanics into architectural choices.
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.
The lab uses only local files plus small immutable Central dependencies. A repository manager or hosted CI system is optional, not required.
- 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.