Chapter 11Lesson 02~195 minutes

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.

Imported BOMDependency TreeEffective POMConvergenceDisposable Lab

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.
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. Disposable workspace 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-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
If the wrapper came from Chapter 04, verify its distribution URL/checksum before executing it. Do not substitute an arbitrary downloaded script. The local repository path .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?

Why can the child omit a dependency version?

What proves that a BOM entry is active but not automatically declared?

Why use usedDependencies for App B instead of disabling dependency analysis?

What state is safe to delete at the end?

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.

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.