Checkpoint Lab — Maven dependencyManagement, BOMs, Version Alignment, Enforcer, and Dependency Analysis
Create and consume a local Maven BOM from two modules, inject and repair a version divergence, and document a bytecode-analysis false positive without weakening the dependency-quality gate.
This checkpoint proves the operating model end to end. You will publish a small BOM to disposable local state, consume it from two modules, prove managed versus declared edges, inject a real version divergence, repair it, and retain a justified dependency-analysis exception for reflective runtime use.
Learning objectives
- Build a standalone BOM and two governed consumer modules.
- Predict and verify managed versions versus actual graph edges.
- Capture Enforcer evidence for a deliberate dependency divergence.
- Repair divergence by returning version authority to the BOM.
- Document and gate one bytecode-analysis false positive without deleting a real runtime dependency.
~/.m2,
global settings, production CI, or real artifact repository is
modified.
1. Checkpoint acceptance contract
The checkpoint is complete only when all of these statements are independently evidenced:
| Invariant | Evidence |
|---|---|
| BOM policy exists independently from application dependencies | platform-bom/pom.xml + installed POM in .lab-m2. |
| Children declare their direct dependencies but omit managed versions | Source POMs + effective POM comparison. |
| Resolved graph is convergent after repair | dependency tree + successful Enforcer execution. |
| Injected 3.9 direct request is rejected | Captured Enforcer failure with conflicting paths. |
| Reflection dependency remains intentional | AppB runtime mechanism + usedDependencies rationale + clean analysis gate. |
| Freshness does not rely on normal user state | All commands use the same disposable .lab-m2 repository. |
2. Setup 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-bom-checkpoint/{platform-bom,apps/app-a/src/main/java/dev/academy/governance,apps/app-b/src/main/java/dev/academy/governance,evidence}
cd maven-bom-checkpoint
./mvnw -v | tee evidence/maven-version.txt
java -version 2> evidence/java-version.txt
printf '%s\n' 'Prediction 1: dependencyManagement alone creates no child edge.' 'Prediction 2: app-a resolves Commons Lang 3.12.0 when its version is omitted.' 'Prediction 3: direct Commons Lang 3.9 will make dependencyConvergence fail.' > evidence/predictions.txt
3. Create the BOM and consumers
<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>
<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>
<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);
}
}
4. Establish the clean baseline
set -euo pipefail
./mvnw -Dmaven.repo.local=.lab-m2 -f platform-bom/pom.xml install | tee evidence/01-bom-install.log
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/pom.xml clean verify | tee evidence/02-baseline-verify.log
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/app-a/pom.xml help:effective-pom -Dverbose > evidence/03-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/04-app-a-tree.txt
grep -n "commons-text\|commons-lang3" evidence/03-app-a-effective-pom.xml evidence/04-app-a-tree.txt
Verify prediction 1 by observing that the BOM's two managed coordinates exist in the effective model, while the graph includes only dependencies declared by App A plus their transitives. Verify prediction 2 by finding Commons Lang 3.12.0 in the resolved tree.
5. Inject one deliberate version divergence
apps/app-a/pom.xml. Add the explicit version shown
below to App A's existing Commons Lang dependency. Do not change the
BOM.
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
set +e
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/pom.xml verify > evidence/05-divergence-verify.log 2>&1
status=$?
set -e
printf 'divergence verify exit=%s\n' "$status" | tee evidence/05-divergence-exit.txt
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/app-a/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree -Dincludes=org.apache.commons:commons-lang3 > evidence/06-divergence-tree.txt || true
grep -n "dependency convergence\|commons-lang3\|3.9\|3.12.0" evidence/05-divergence-verify.log evidence/06-divergence-tree.txt || true
Prediction 3 should hold: App A directly requests 3.9 while Commons Text's path requests/manages the 3.12.0 family, so the strict convergence rule exposes conflicting version paths instead of accepting mediation silently.
6. Repair divergence by restoring one version authority
Remove only <version>3.9</version> from App
A. Keep the direct dependency because App A's source really uses
StringUtils. The BOM again supplies 3.12.0, so
declaration remains local while version policy remains centralized.
set -euo pipefail
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/pom.xml clean verify | tee evidence/07-repaired-verify.log
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/app-a/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree -Dincludes=org.apache.commons:commons-lang3 | tee evidence/08-repaired-tree.txt
7. Prove and document the declaration-quality false positive
App B references Commons Lang only through a class-name string and
reflection. Without its narrow
usedDependencies configuration, bytecode analysis can
classify the direct dependency as unused. Do not delete it: the
class must exist at runtime for Class.forName to
succeed.
set -euo pipefail
./mvnw -Dmaven.repo.local=.lab-m2 -f apps/app-b/pom.xml org.apache.maven.plugins:maven-dependency-plugin:3.11.0:analyze -Dverbose | tee evidence/09-app-b-analysis.txt
grep -n "commons-lang3\|unused\|used" evidence/09-app-b-analysis.txt || true
The documented usedDependencies entry is not permission
to ignore all unused declarations. It records one known
bytecode-analysis limitation and keeps
failOnWarning=true useful for everything else.
8. Verification checklist
- BOM installed only into
.lab-m2. - Both consumer POMs omit managed dependency versions in the clean state.
- Effective POM shows imported management.
- App A tree proves the actual dependency edges and selected versions.
- Deliberate 3.9 divergence produces a non-zero governed build with preserved Enforcer evidence.
- Removing only the duplicated version restores convergence.
- App B's reflective dependency is retained with a narrow analysis declaration.
- No dynamic/range/SNAPSHOT dependency was introduced as a production default.
- No normal user cache, credentials, remote repository, or CI system was modified.
9. Cleanup and rollback
cd ..
rm -rf maven-bom-checkpoint
If you want to keep the evidence for review, copy only
evidence/ to a neutral training-output directory before
deleting the workspace. Do not preserve generated
target/ directories as authoritative source state.
Knowledge check
Why does the checkpoint keep App A’s direct Commons Lang dependency after removing version 3.9?
Because the source directly uses StringUtils. The declaration belongs to App A; only the version authority belongs to the BOM.
What proves that a managed dependency is different from a declared dependency?
The effective management table can contain a coordinate that is absent from the project’s direct dependency graph until a dependency edge is declared or encountered transitively.
Why should the deliberate divergence be captured before repair?
The Enforcer error paths are evidence of the exact policy violation and prove that the gate detects drift rather than merely passing the repaired state.
Why is deleting Commons Lang from App B the wrong response to an “unused” warning?
App B loads the class reflectively at runtime; bytecode analysis cannot necessarily see the string-based relationship.
Does a successful Enforcer run prove artifact integrity?
No. It proves configured graph/environment rules. Artifact checksum/signature/provenance verification is a different supply-chain control.
What production capability does Chapter 11 add?
Explainable dependency-platform governance: central version policy, visible declaration edges, executable graph rules, dependency-quality analysis, and controlled exceptions backed by evidence.
10. Production operating model and Chapter 12 bridge
Chapter 11 adds a governed dependency platform to the
build-engineering operating model: version policy can be published
independently, consumers declare only what they use, graph drift can
fail CI, and analysis exceptions remain explicit. Chapter 12 moves
from dependency inputs to artifact outputs—packaging,
install, deploy, distribution management,
signing, and repository publication.
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 checkpoint intentionally uses an isolated local install as the publication boundary. Remote deployment and signing are deferred to Chapter 12.
- 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.