Chapter 09Lesson 05~210 minutes

Checkpoint Lab — Maven Multi-Module Projects, Parent POMs, Aggregation, Inheritance, and Reactor Builds

Build and explain a three-module reactor, draw aggregation/parent/dependency graphs separately, verify targeted selection, inject a cycle and parent-path failure, and prove the clean fix from an isolated repository.

Checkpoint LabThree ModulesGraph EvidenceTargeted BuildClean-Room Verification

Learning objectives

  • Create a root parent/aggregator plus exactly three child modules with distinct coordinates.
  • Draw aggregation, parent, and dependency graphs separately before building.
  • Predict and verify reactor order and targeted project selection from a clean isolated repository.
  • Inject and diagnose both a dependency cycle and a parent-path error while preserving evidence.
  • Prove installed coordinates and build outputs without relying on stale normal user state.
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, Help Plugin 3.5.2, Dependency Plugin 3.11.0, Compiler Plugin 3.15.0, Surefire 3.5.6, and JAR Plugin 3.5.1. Every lab uses a project-local isolated Maven repository. No normal ~/.m2, global settings, production repository, or CI configuration is modified.
Cross-platform note: POSIX examples use ./mvnw, grep, find, and sha256sum. On Windows use mvnw.cmd, Select-String, Get-ChildItem, and Get-FileHash. Maven reactor semantics are cross-platform; shell quoting and path separators are not.

1. Checkpoint acceptance contract

You will create one root graph-parent POM plus three child modules: model, service, and app. The child dependency chain is app → service → model. The root deliberately lists them in reverse logical order so the reactor must sort from actual dependency relationships.

Invariant Evidence
Exactly three child modules Root <modules> and filesystem.
All three inherit root policy Child <parent> plus effective POMs.
Dependency chain app → service → model Dependency trees and POM declarations.
Build order model → service → app Reactor Build Order/Summary.
Targeted app build includes prerequisites Fresh repo + -pl :app -am.
Broken cycle fails for graph reason Preserved failure log.
Broken parent path fails for model-resolution reason Separate preserved failure log from fresh repo.

2. Setup and preflight

Use a disposable directory. Copy or generate the already verified Maven Wrapper at the root; do not modify a global Maven installation or normal local repository.

mkdir -p reactor-checkpoint/{model/src/main/java/dev/academy/graph,service/src/main/java/dev/academy/graph,app/src/main/java/dev/academy/graph,evidence}
cd reactor-checkpoint
export REPO_CLEAN="$PWD/.lab-m2-clean"
export REPO_TARGET="$PWD/.lab-m2-target"
export REPO_CYCLE="$PWD/.lab-m2-cycle"
export REPO_PARENT="$PWD/.lab-m2-parent"
./mvnw -v

3. Author the root and three child POMs

The root is both parent and aggregator in this checkpoint. The root's module list is app, service, model; dependency edges should force the opposite execution direction.

<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.graph</groupId>
  <artifactId>graph-parent</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>
  <modules>
    <module>app</module>
    <module>service</module>
    <module>model</module>
  </modules>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.outputTimestamp>2026-08-23T00:00:00Z</project.build.outputTimestamp>
  </properties>
  <dependencyManagement>
    <dependencies>
      <dependency><groupId>dev.academy.graph</groupId><artifactId>model</artifactId><version>${project.version}</version></dependency>
      <dependency><groupId>dev.academy.graph</groupId><artifactId>service</artifactId><version>${project.version}</version></dependency>
    </dependencies>
  </dependencyManagement>
  <build><pluginManagement><plugins>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.15.0</version></plugin>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version></plugin>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.5.1</version></plugin>
  </plugins></pluginManagement></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.graph</groupId><artifactId>graph-parent</artifactId><version>1.0.0</version><relativePath>../pom.xml</relativePath>
  </parent>
  <artifactId>model</artifactId>
  
  <build><plugins>
    <plugin><artifactId>maven-compiler-plugin</artifactId></plugin>
    <plugin><artifactId>maven-surefire-plugin</artifactId></plugin>
    <plugin><artifactId>maven-jar-plugin</artifactId></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.graph</groupId><artifactId>graph-parent</artifactId><version>1.0.0</version><relativePath>../pom.xml</relativePath>
  </parent>
  <artifactId>service</artifactId>
  <dependencies><dependency><groupId>dev.academy.graph</groupId><artifactId>model</artifactId></dependency></dependencies>
  <build><plugins>
    <plugin><artifactId>maven-compiler-plugin</artifactId></plugin>
    <plugin><artifactId>maven-surefire-plugin</artifactId></plugin>
    <plugin><artifactId>maven-jar-plugin</artifactId></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.graph</groupId><artifactId>graph-parent</artifactId><version>1.0.0</version><relativePath>../pom.xml</relativePath>
  </parent>
  <artifactId>app</artifactId>
  <dependencies><dependency><groupId>dev.academy.graph</groupId><artifactId>service</artifactId></dependency></dependencies>
  <build><plugins>
    <plugin><artifactId>maven-compiler-plugin</artifactId></plugin>
    <plugin><artifactId>maven-surefire-plugin</artifactId></plugin>
    <plugin><artifactId>maven-jar-plugin</artifactId></plugin>
  </plugins></build>
</project>

4. Add one compile-time dependency at each layer

model defines a record, service consumes it, and app consumes the service. This makes each reactor edge observable through compilation.

package dev.academy.graph;
public record Message(String value) {}
package dev.academy.graph;
public final class MessageService {
    private MessageService() {}
    public static Message create(String value) { return new Message(value); }
}
package dev.academy.graph;
public final class App {
    private App() {}
    public static void main(String[] args) {
        System.out.println(MessageService.create("reactor-ok").value());
    }
}

5. Draw the three graphs separately

Do this before running Maven. The aggregation graph answers collection. The parent graph answers inheritance. The dependency graph answers compile/runtime consumption and drives the critical reactor order.

Aggregation graph
flowchart LR
 Root[graph-parent] -->|module| App[app]
 Root -->|module| Service[service]
 Root -->|module| Model[model]
Inheritance and dependency graphs
flowchart TD
 subgraph ParentGraph[Parent inheritance]
   AppP[app] -->|parent| RootP[graph-parent]
   ServiceP[service] -->|parent| RootP
   ModelP[model] -->|parent| RootP
 end
 subgraph DependencyGraph[Artifact dependencies]
   AppD[app] -->|depends on| ServiceD[service]
   ServiceD -->|depends on| ModelD[model]
 end

6. Record predictions before execution

Prediction 1: full build order should put model before service before app, despite reverse module declaration.

Prediction 2: -pl :app -am from a fresh repository should include the dependency chain required to build app from source.

Prediction 3: adding an app dependency to model creates a cycle and should fail before normal compilation can establish a valid project order.

Prediction 4: breaking service's parent relativePath should fail model construction from a fresh repository rather than silently inherit the root.

7. Verify the full reactor from empty state

Capture the full build and effective models. The isolated repository prevents previously installed copies from serving as hidden inputs.

set -o pipefail
rm -rf "$REPO_CLEAN"
./mvnw -Dmaven.repo.local="$REPO_CLEAN" clean verify 2>&1 | tee evidence/full.log
grep -A 10 "Reactor Build Order" evidence/full.log || true
grep -A 10 "Reactor Summary" evidence/full.log || true
./mvnw -Dmaven.repo.local="$REPO_CLEAN" -pl :app -am dependency:tree > evidence/app-dependency-tree.txt
./mvnw -Dmaven.repo.local="$REPO_CLEAN" -pl :service help:effective-pom -Doutput="$PWD/evidence/effective-service.xml"

Verify the dependency tree contains service, and service's dependency tree contains model. Verify inherited compiler/plugin policy in the effective POM.

8. Inspect artifact coordinates and bytes

After package/verify, each module owns its own artifact. Record checksums. Then use a separate isolated repository for install and inspect the installed coordinate paths.

sha256sum model/target/model-1.0.0.jar service/target/service-1.0.0.jar app/target/app-1.0.0.jar | tee evidence/jars.sha256
./mvnw -Dmaven.repo.local="$REPO_CLEAN" install
find "$REPO_CLEAN/dev/academy/graph" -maxdepth 5 -type f -print | sort > evidence/installed-files.txt

The parent has coordinate dev.academy.graph:graph-parent:1.0.0:pom; each child has its own jar coordinate. Aggregation does not merge them into one artifact.

9. Target app with prerequisites from a different fresh repository

Use another empty repository so the preceding install cannot influence this test.

set -o pipefail
rm -rf "$REPO_TARGET"
./mvnw -Dmaven.repo.local="$REPO_TARGET" -pl :app -am verify 2>&1 | tee evidence/target-app.log
grep -A 10 "Reactor Build Order" evidence/target-app.log || true
grep -A 10 "Reactor Summary" evidence/target-app.log || true

Compare the actual selected project list with Prediction 2. Your acceptance criterion is not a memorized count; it is that every required in-reactor prerequisite for app is present and ordered before its consumer.

10. Inject a dependency cycle and diagnose it

Temporarily add this dependency to model/pom.xml. It makes model → app → service → model.

<dependencies>
  <dependency>
    <groupId>dev.academy.graph</groupId>
    <artifactId>app</artifactId>
    <version>${project.version}</version>
  </dependency>
</dependencies>
rm -rf "$REPO_CYCLE"
set +e
./mvnw -Dmaven.repo.local="$REPO_CYCLE" validate > evidence/cycle-failure.log 2>&1
rc=$?
set -e
printf 'cycle_exit=%s
' "$rc"
grep -nEi 'cycle|cyclic|reactor|BUILD FAILURE' evidence/cycle-failure.log || true

Restore the original model/pom.xml. Do not reorder modules as a “fix”; a graph cycle cannot be topologically sorted.

11. Inject a parent path failure independently

After restoring the cycle, change only service's parent path to a nonexistent local POM. Use yet another fresh repository so an installed parent cannot mask the problem.

<relativePath>../missing-parent.xml</relativePath>
rm -rf "$REPO_PARENT"
set +e
./mvnw -Dmaven.repo.local="$REPO_PARENT" validate > evidence/parent-failure.log 2>&1
rc=$?
set -e
printf 'parent_exit=%s
' "$rc"
grep -nEi 'parent|relativePath|Non-resolvable|BUILD FAILURE' evidence/parent-failure.log || true

Restore ../pom.xml and verify again from the same clean repository path after clearing only that disposable path.

12. Final controlled verification

After restoring both faults, use a new or cleared lab repository and rerun the targeted app build. Compare its Reactor Build Order, dependency tree, and JAR checksums with the healthy baseline.

rm -rf "$REPO_TARGET"
set -o pipefail
./mvnw -Dmaven.repo.local="$REPO_TARGET" -pl :app -am clean verify 2>&1 | tee evidence/final.log
sha256sum model/target/model-1.0.0.jar service/target/service-1.0.0.jar app/target/app-1.0.0.jar | tee evidence/final-jars.sha256

13. Evidence manifest

Evidence Question answered
full.log What did the complete reactor collect/sort/build?
app-dependency-tree.txt What artifact edges does app actually consume?
effective-service.xml What parent/management/plugin values does service actually inherit?
jars.sha256 What artifact bytes did the healthy build produce?
installed-files.txt Which coordinates were installed into the isolated repository?
target-app.log What did -pl :app -am select from clean state?
cycle-failure.log Did the dependency cycle fail for graph-order reasons?
parent-failure.log Did the broken relativePath fail model/parent resolution?
final.log Did the smallest fixes restore a clean targeted build?

14. Cleanup / rollback

Keep evidence/ only if you want the learning record. Remove the disposable checkpoint tree and its .lab-m2-* repositories when finished. Do not delete or alter normal user Maven state.

Knowledge check

Why are the aggregation, parent, and dependency graphs drawn separately?

Why does the checkpoint reverse app/service/model in the modules list?

The app-only build worked after a previous install but fails from an empty repository. What does that diagnose?

A cycle error appears before compilation. Why is changing -pl or module order not the real fix?

Why test the broken parent path with a separate fresh repository?

What production capability does Chapter 09 add?

15. Production build-engineering model and Chapter 10 bridge

Chapter 09 adds scale without surrendering causality. A production Maven repository can now explain which projects are collected, what policy each child inherits, which artifacts create ordering edges, what a targeted CI build includes, and whether success depends on stale local state. Chapter 10 builds on this reactor model to separate unit and integration testing with Surefire/Failsafe, reports, lifecycle phases, and CI quality evidence across modules.

Official references and version notes

Version-sensitive statements in this lesson were checked against Apache Maven primary documentation on 2026-08-23. The mandatory path uses Maven 3.9.16 through Maven Wrapper 3.3.4, JDK 21 to run Maven, Java 17 as the project release target, Help Plugin 3.5.2, Dependency Plugin 3.11.0, Compiler Plugin 3.15.0, Surefire 3.5.6, and JAR Plugin 3.5.1. Maven 4 terminology is called out only where it differs materially; the hands-on path remains Maven 3.9.16.

The checkpoint intentionally uses reversed module declarations so the reactor's topological sort is observable rather than merely asserted.

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.