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.
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.
~/.m2, global settings, production repository, or CI
configuration is modified.
./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.
flowchart LR Root[graph-parent] -->|module| App[app] Root -->|module| Service[service] Root -->|module| Model[model]
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?
Because they answer different questions: collection, model inheritance, and artifact consumption/build ordering. Conflating them makes failures hard to localize.
Why does the checkpoint reverse app/service/model in the modules list?
To prove the reactor follows instantiated dependency edges for ordering rather than blindly executing directory/module-list order.
The app-only build worked after a previous install but fails from an empty repository. What does that diagnose?
The earlier success depended on installed sibling artifacts; the targeted reactor selection itself was incomplete.
A cycle error appears before compilation. Why is changing -pl or module order not the real fix?
The project dependency graph is unsatisfiable. You must remove/redesign a dependency edge so a topological order exists.
Why test the broken parent path with a separate fresh repository?
An installed parent POM could otherwise satisfy parent resolution and hide the incorrect repository-local relationship.
What production capability does Chapter 09 add?
An explicit multi-project operating model: separate aggregation/inheritance/dependency graphs, reproducible parent policy, graph-correct reactor selection, and clean-room diagnostics for large Maven repositories.
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.
- Maven — Guide to Working with Multiple Modules (Maven 3)
- Maven — POM Reference: Inheritance and Aggregation
- Maven — Introduction to the POM
- Maven — Maven Model Builder
- Maven — CLI Reference
- Maven Help Plugin 3.5.2
- Maven Dependency Plugin 3.11.0
- Maven Compiler Plugin 3.15.0
- Maven Surefire Plugin 3.5.6
- Maven JAR Plugin 3.5.1
- Apache Maven Wrapper
- Maven 3.9.16 Release Notes
- Maven 4 Multi-Subproject Guide — comparison only
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.