Maven Multi-Module Projects, Parent POMs, Aggregation, Inheritance, and Reactor Builds: Diagnostics, Failure Modes, Security, and Performance
Diagnose multi-module failures from graph evidence: aggregation without inheritance, wrong parent paths, dependency cycles, incomplete targeted builds, stale installed artifacts, and inherited configuration drift.
Learning objectives
- Apply a repeatable diagnostic sequence to multi-module failures.
-
Diagnose aggregation-without-inheritance and wrong-parent
relativePathbehavior. - Interpret reactor-cycle and incomplete-selection failures without deleting normal caches.
- Prove when an installed sibling artifact masks a broken reactor selection.
- Separate resolution, model, reactor, compilation/test, and performance evidence.
~/.m2, global settings, production repository, or CI
configuration is modified.
1. Diagnostic sequence: preserve graph evidence before changing state
- Preserve concise error output and Reactor Build Order/Summary.
- Confirm wrapper, Maven, and JDK identity.
-
Inspect aggregator
<modules>and child<parent>. - Inspect effective POM and actual dependency tree.
-
Inspect selected project set:
-pl,-am,-amd,-rf,-N. - Repeat with a fresh project-local repository when stale installs could mask the issue.
- Inspect the specific compiler/test/plugin failure only after model/reactor scope is correct.
- Apply the least destructive correction that fixes the proven cause, then verify from controlled state.
2. Failure: aggregated module does not inherit expected policy
Symptom: the module appears in the reactor but its effective POM
lacks the root's compiler/plugin/dependency management. The root
lists the module, so aggregation works; however, the child's
<parent> points somewhere else or is absent.
| Evidence | Interpretation |
|---|---|
| Module appears in Reactor Build Order | Aggregator collected it. |
| Child effective POM lacks root property/plugin management | Inheritance relationship is absent/different. |
| Child POM has another parent | Legal architecture; expectation was wrong. |
| Fix | Either change the intended parent deliberately or stop expecting aggregation to imply inheritance. |
3. Failure: relativePath resolves the wrong or no local parent
Intentionally change a child to
<relativePath>../not-the-parent.xml</relativePath>. From a fresh isolated local repository, Maven should no longer be
able to use the intended repository-local parent through that path.
Preserve the model-building error before editing anything.
<parent>
<groupId>dev.academy.reactor</groupId>
<artifactId>reactor-lab-parent</artifactId>
<version>1.0.0</version>
<relativePath>../not-the-parent.xml</relativePath>
</parent>
4. Failure: dependency cycle prevents a valid reactor order
If A depends on B and B depends on A, no topological ordering
satisfies both. Maven should report a cyclic project relationship
during project sorting/build setup. Treat the cycle as an
architecture defect, not an ordering typo in
<modules>.
flowchart LR A[model] -->|depends on| C[app] C -->|depends on| S[service] S -->|depends on| A
The correction is to remove or invert a dependency, extract an interface/shared model into a lower-level module, or redesign the coupling. Reordering module declarations cannot solve a directed cycle.
5. Failure: targeted build omits prerequisites
Start from an empty lab repository and select only the application
without -am. If the application depends on an
in-reactor library that is not selected and not available from a
repository, resolution should fail. This is the exact class of
problem --also-make exists to prevent.
REPO_EMPTY="$PWD/.lab-m2-empty"
rm -rf "$REPO_EMPTY"
set +e
./mvnw -Dmaven.repo.local="$REPO_EMPTY" -pl :greeter-app package > evidence/app-only-failure.log 2>&1
rc=$?
set -e
printf 'exit=%s
' "$rc"
grep -nE 'Could not resolve|greeter-lib|BUILD FAILURE' evidence/app-only-failure.log || true
Repair by selecting the application plus its prerequisites:
-pl :greeter-app -am. Do not copy a prebuilt sibling
JAR into place.
6. Failure masked by installed local state
Now compare two environments. In repository A, run
install for the whole reactor, then an app-only build
may resolve the library from that local repository. In fresh
repository B, the same app-only selection exposes the missing
producer. The source tree did not change; the hidden state did.
| Run | Local state | What it proves |
|---|---|---|
Full install then app-only build |
Sibling artifact installed | App can resolve a repository artifact; does not prove reactor selection is complete. |
| App-only build from fresh repo | No sibling artifact | Tests whether selection itself contains/provides required producer. |
App + -am from fresh repo |
Producer included from source | Strong evidence of graph-correct targeted build. |
7. Resume-from is recovery, not a dependency solver
-rf/--resume-from can continue a reactor
at a project after a failure. It does not redesign the dependency
graph or guarantee prerequisite artifacts are available if you
changed state between runs. Before resuming in CI, understand which
earlier modules were successfully built and where their outputs
live.
8. Security-sensitive multi-module changes
Review changes to external parent coordinates, module paths, build extensions, plugin repositories, inherited plugins, repository settings, or CI-selected project expressions. A malicious or mistaken parent/module change can execute code across many projects.
9. Performance: measure collection, resolution, compilation, tests, and packaging separately
Targeted builds can reduce compilation/test work, but cold dependency/plugin resolution and model construction still cost time. A warm developer machine is not a valid benchmark for an ephemeral CI agent. Compare full and selected builds with the same isolated repository/cache assumptions before declaring one strategy faster.
| Observation | Likely dimension |
|---|---|
| First run slow, second much faster | Cold dependency/plugin resolution or filesystem/JIT effects. |
| Many unaffected modules compile/test | Selection policy too broad or full reactor chosen. |
| Model fails before compilation | Parent/aggregator/reactor configuration problem. |
| Only tests dominate | Test workload; not primarily reactor sorting. |
| Package/install dominates | Packaging/local repository side effects; inspect separately. |
10. Minimal correction playbook
| Symptom | Do first | Avoid |
|---|---|---|
| Missing inherited config | Compare child parent + effective POM | Adding duplicate config blindly to every child |
| Parent not resolvable | Check coordinates + relativePath + fresh repo | Installing arbitrary parent into normal cache |
| Cycle | Draw dependency graph and break architectural cycle | Reordering modules |
| Selected app cannot resolve sibling | Use -am or correct repository strategy |
Copying JARs manually |
| Works only after install | Repeat with fresh isolated repo | Calling stale state “reproducible” |
Knowledge check
A module is in Reactor Build Order but lacks root compiler configuration. What relationship should you inspect first?
Its <parent> and effective POM. Aggregation
proves collection, not inheritance.
Can reordering the modules list repair A→B→A dependency cycle?
No. A cycle has no valid topological order; the dependency architecture must change.
Why repeat an app-only targeted build with a fresh repository?
To reveal whether success depended on a previously installed sibling artifact instead of the selected reactor graph.
What does -rf solve?
It changes where a reactor resumes after a failure. It does not create missing dependency edges or guarantee prerequisites are available.
Why are parent POM changes security-sensitive?
Parents can alter plugins, repositories, profiles, properties, dependency/plugin management, and other build behavior across many children.
11. Summary and next bridge
Multi-module troubleshooting is graph troubleshooting. Preserve the model, reactor selection, and local-state evidence before touching files. Lesson 5 integrates those skills into a three-module checkpoint where the same repository is observed under full, targeted, broken-cycle, and broken-parent conditions.
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.