Chapter 11Lesson 05~220 minutes

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.

Checkpoint LabTwo ModulesVersion DivergenceFalse PositiveEvidence

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.
Current baseline — verified 2026-08-23. Labs use 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. All Maven resolution uses a disposable project-local repository. No normal ~/.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

Cross-platform note: POSIX examples use ./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

Controlled break: edit only the disposable 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?

What proves that a managed dependency is different from a declared dependency?

Why should the deliberate divergence be captured before repair?

Why is deleting Commons Lang from App B the wrong response to an “unused” warning?

Does a successful Enforcer run prove artifact integrity?

What production capability does Chapter 11 add?

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.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.