Chapter 09Lesson 02~190 minutes

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.

Multi-Module Lab-pl / -amEffective POMDependency ManagementPlugin Management

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 -amd with 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.
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. 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
If ./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.

Expected relationship and build direction
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
Management is not activation. The parent's 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?

What does -am add to a -pl app selection?

What does -amd do when the selected project is a shared library?

Why inspect effective POMs after moving versions into management sections?

Why can a prior install make a bad targeted build look valid?

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.

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.