Maven Multi-Module Projects, Parent POMs, Aggregation, Inheritance, and Reactor Builds: Guided Hands-On Workflow and Core Operations
Build a disposable parent/aggregator with library and application modules, inspect reactor order, use targeted reactor selection safely, centralize dependency/plugin policy, and verify effective child models.
Learning objectives
- Create a disposable root aggregator/parent with library and application modules.
- Verify that a real dependency edge overrides intentionally reversed module-list order.
-
Use
-pl,-am, and-amdwith observable reactor summaries. - Move dependency and plugin policy into management sections without confusing management with activation.
- Inspect child effective POMs and isolated-repository state before and after install.
~/.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. Preflight and disposable workspace
Create the lab outside valuable repositories. The examples assume the Maven Wrapper files from Chapter 04 are present at the root and pinned to Maven 3.9.16. If you regenerate the wrapper, re-verify its distribution URL/checksum before trusting it.
mkdir -p reactor-lab/{greeter-lib/src/main/java/dev/academy/reactor,greeter-app/src/main/java/dev/academy/reactor,evidence}
cd reactor-lab
export LAB_REPO="$PWD/.lab-m2"
./mvnw -v
./mvnw -v does not report Maven 3.9.16 and the
intended JDK, stop. Tool identity is an input to every later
observation.
2. Create a root that is both aggregator and parent
The root uses packaging=pom. Its
<modules> intentionally lists the application
before the library so the lab can prove that the dependency
graph—not the human ordering—controls the meaningful build edge.
<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.reactor</groupId>
<artifactId>reactor-lab-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<!-- Intentionally list app before library. Dependency edges, not directory order,
must force greeter-lib before greeter-app. -->
<modules>
<module>greeter-app</module>
<module>greeter-lib</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.reactor</groupId>
<artifactId>greeter-lib</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>
3. Create child POMs with explicit inheritance
Both child POMs inherit group/version/compiler policy from the root.
They explicitly reference the root with relativePath.
The application declares a real dependency on the library but omits
the version because the parent's
dependencyManagement supplies it.
<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.reactor</groupId>
<artifactId>reactor-lab-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>greeter-lib</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.reactor</groupId>
<artifactId>reactor-lab-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>greeter-app</artifactId>
<dependencies>
<dependency>
<groupId>dev.academy.reactor</groupId>
<artifactId>greeter-lib</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 the smallest source flow
The library owns a reusable API; the application consumes it. That dependency is the ordering edge we want Maven to discover.
package dev.academy.reactor;
public final class Greeting {
private Greeting() {}
public static String message(String name) {
return "Hello, " + name + "!";
}
}
package dev.academy.reactor;
public final class App {
private App() {}
public static void main(String[] args) {
System.out.println(Greeting.message("reactor"));
}
}
5. Predict before executing
Write down this prediction before the build: even though
greeter-app appears first in
<modules>, Maven should build
greeter-lib before greeter-app because the
app has an instantiated project dependency on the library. The root
POM participates as the aggregator/parent project.
flowchart LR Root[reactor-lab-parent] -->|aggregates| App[greeter-app] Root -->|aggregates| Lib[greeter-lib] App -->|inherits| Root Lib -->|inherits| Root Lib -->|must exist before| App
6. Run a full reactor build and capture the summary
Use an isolated repository so the build cannot succeed because
another lab installed the module previously. Keep evidence outside
module target/ directories.
set -o pipefail
./mvnw -Dmaven.repo.local="$LAB_REPO" clean verify 2>&1 | tee evidence/full-reactor.log
grep -A 8 "Reactor Build Order" evidence/full-reactor.log || true
grep -A 8 "Reactor Summary" evidence/full-reactor.log || true
find greeter-lib/target greeter-app/target -maxdepth 2 -type f -print
Expected causality: Maven collects the root and two modules, sorts the library before the application, compiles each module in its own lifecycle context, and creates two separate JAR artifacts.
7. Inspect inherited and managed state
The child POM files are intentionally compact. The effective POM proves which properties and plugin versions they actually use after inheritance and management are applied.
./mvnw -Dmaven.repo.local="$LAB_REPO" -pl :greeter-lib help:effective-pom -Doutput="$PWD/evidence/effective-lib.xml"
./mvnw -Dmaven.repo.local="$LAB_REPO" -pl :greeter-app help:effective-pom -Doutput="$PWD/evidence/effective-app.xml"
grep -nE 'maven.compiler.release|maven-(compiler|surefire|jar)-plugin|greeter-lib' evidence/effective-*.xml
pluginManagement pins configuration/version defaults.
The child <plugins> declarations activate those
plugins as explicit project build plugins.
8. Target the application correctly with -pl + -am
-pl :greeter-app selects the application.
-am expands the selection to reactor projects required
by it. That makes the dependency closure explicit.
set -o pipefail
rm -rf "$LAB_REPO"
./mvnw -Dmaven.repo.local="$LAB_REPO" -pl :greeter-app -am package 2>&1 | tee evidence/app-also-make.log
grep -A 8 "Reactor Build Order" evidence/app-also-make.log || true
The fresh repository is deliberate. If a prerequisite is not part of the selected build, no old installed copy should be available to hide that fact.
9. Select a library and also build dependents
For change-impact validation, select the library and add
-amd. Maven includes reactor projects that depend on
the selected project, so the application should join the build. This
is a different graph direction from -am.
set -o pipefail
./mvnw -Dmaven.repo.local="$LAB_REPO" -pl :greeter-lib -amd test 2>&1 | tee evidence/lib-dependents.log
grep -A 8 "Reactor Build Order" evidence/lib-dependents.log || true
10. Distinguish reactor availability from install side effects
package creates module artifacts under each module's
target/. install additionally writes the
project POM/artifact into the configured local repository. That
install can later satisfy a module build even when the producer is
not in the current selected reactor—useful sometimes, but dangerous
evidence if you are trying to prove the reactor relationship itself.
./mvnw -Dmaven.repo.local="$LAB_REPO" install
find "$LAB_REPO/dev/academy/reactor" -maxdepth 5 -type f -print
11. Challenge: choose the control, do not copy a sequence
You changed only greeter-lib and want CI to test both
the library and all in-repository modules that depend on it. Which
selector is semantically correct?
Answer after reasoning: select the library with
-pl :greeter-lib and expand downstream with
-amd. -am would travel in the opposite
direction—toward prerequisites of the selected library.
12. Verification and cleanup
Verify that both JARs exist, effective POMs show inherited
release/plugin policy, and the logs show the expected project
selection. Remove only the disposable workspace and
.lab-m2 when finished; do not clean normal user
repositories.
Knowledge check
Why was greeter-app listed before greeter-lib in the modules list?
To prove the stronger project dependency edge controls build order; module-list order is only a fallback.
What does -am add to a -pl app selection?
Reactor projects required by the selected app—its in-reactor prerequisite closure.
What does -amd do when the selected project is a shared library?
It includes reactor projects that depend on that library, useful for downstream impact testing.
Why inspect effective POMs after moving versions into management sections?
Because the compact child POM no longer shows every effective property/plugin version; the effective model proves inheritance and management results.
Why can a prior install make a bad targeted build look valid?
The local repository may satisfy a module dependency even though the current reactor selection omitted the producer.
13. Summary and next bridge
You can now create a small reactor, prove its order, target the correct graph direction, and inspect inherited policy. Lesson 3 moves from mechanics to architecture: when should one root be both parent and aggregator, and when should those responsibilities be separated?
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.
- 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.