Chapter 05Lesson 04~125 minutes

Maven POM Structure, Coordinates, Packaging, Build Lifecycle, Phases, and Goals: Diagnostics, Failure Modes, Security, and Performance

Diagnose lifecycle and POM failures by preserving evidence, inspecting the effective model and bindings, and repairing phase/goal, packaging, inheritance, install, and duplicate-execution mistakes safely.

DiagnosticsEffective POMOrderingInstall vs DeploySecurity

Learning objectives

  • Apply a repeatable evidence-first diagnostic sequence to Maven lifecycle and POM failures.
  • Recognize phase/goal confusion and duplicate/default plugin execution from build logs.
  • Diagnose packaging and inherited-effective-POM mismatches before editing plugins or deleting caches.
  • Prove that install is local and distinct from remote deploy.
  • Repair a deliberately misplaced harmless goal by reasoning about phase ordering.
Current baseline — checked 2026-08-23. Required labs use Maven 3.9.16 through Maven Wrapper 3.3.4, JDK 21 to run Maven, and Java 17 as the compilation release target. For Maven 3.9.16 jar packaging, core default bindings currently include Resources 3.4.0, Compiler 3.15.0, Surefire 3.5.4, JAR 3.5.0, Install 3.1.4, and Deploy 3.1.4. The lab uses a disposable project-local repository path; no remote publication, paid repository manager, or hosted CI is required.

1. Diagnostic sequence — preserve causality first

# 1. Preserve concise identity and failure output.
./mvnw --version
./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository" package \
  | tee target/failure.log

# 2. Inspect the declared/effective model.
./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository" \
  org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom \
  -Dverbose -Doutput=target/effective-pom.xml

# 3. Extract executed goals; do not erase the original log.
grep -E '^\[INFO\] --- .*:[0-9].*:.* \(' target/failure.log || true

# 4. Inspect target and the isolated repository only after the model.
find target -maxdepth 3 -type f -print | sort
find .lab-m2/repository -maxdepth 6 -type f -print | sort | head -100

The order matters. If you delete caches or target before preserving evidence, you can remove the state that explains the failure. Correct the smallest causal configuration, then rerun in the same controlled environment.

2. Failure: a goal is confused with a phase

Symptom: an operator runs compiler:compile expecting the same preparation as compile. Java classes appear, but copied resources do not.

Interpretation: compile is a default-lifecycle phase. compiler:compile is one goal. Direct goal invocation does not imply process-resources.

./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository" clean
./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository" \
  org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile

test -f target/classes/build.properties || echo "resource missing"

# Least-destructive correction: invoke the lifecycle contract you actually need.
./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository" compile
test -f target/classes/build.properties && echo "resource restored"

3. Failure: a goal runs twice because a default binding and explicit execution overlap

Symptom: the log contains both (default-resources) and a second custom resources execution in process-resources. The build may still succeed, but it performs duplicate work and can apply filtering/configuration twice.

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-resources-plugin</artifactId>
      <version>3.4.0</version>
      <executions>
        <execution>
          <id>duplicate-resources</id>
          <phase>process-resources</phase>
          <goals><goal>resources</goal></goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

For jar packaging, Maven 3.9.16 already binds resources:resources to process-resources. If the second execution adds no intentional behavior, remove it. If you need configuration, configure the plugin/default execution instead of creating an accidental duplicate.

4. Failure: packaging does not match the expected artifact

Symptom: package succeeds, but no JAR exists. The declared POM says <packaging>pom</packaging>.

Interpretation: for pom packaging, Maven core does not bind a normal artifact-producing goal at package; install/deploy operate on POM metadata. The build is behaving according to its model.

grep -n '<packaging>' pom.xml || echo "packaging omitted -> defaults to jar"
./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom \
  -Doutput=target/effective-pom.xml
grep -n '<packaging>' target/effective-pom.xml | head

Repair the architecture, not the symptom. Change packaging to jar only if the project is genuinely a JAR-producing module; do not bolt jar:jar onto a parent/aggregator merely to satisfy an incorrect CI file expectation.

5. Failure: the effective POM contains inherited behavior you did not expect

Symptom: the child POM does not show a compiler release or plugin version that appears in the build log. A parent POM provides it.

Run help:effective-pom -Dverbose. The verbose output annotates XML elements with origin information, allowing you to distinguish child declarations, parent inheritance, profile activation, and defaults. Do not “fix” the child by duplicating inherited configuration until you know which layer owns policy.

./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom \
  -Dverbose -Doutput=target/effective-pom.xml

grep -n -B2 -A5 -E 'maven-compiler-plugin|<release>|<finalName>' \
  target/effective-pom.xml | head -100

6. Failure: install is mistaken for remote publication

Symptom: a runbook claims “mvn install published version 1.0.0,” but no remote repository contains it.

Interpretation: the Install Plugin writes artifacts into the local repository using coordinates. Remote publication belongs to the later deploy phase and requires repository configuration/credentials. This lesson does not require deployment.

REPO="$PWD/.lab-m2/repository"
rm -rf "$REPO/dev/academy/lifecycle-lab"
./mvnw -Dmaven.repo.local="$REPO" install
find "$REPO/dev/academy/lifecycle-lab/1.0.0" -maxdepth 1 -type f -print | sort
Security boundary: never add real repository credentials to pom.xml merely to test deployment. Publishing/signing are covered later with fake/disposable targets and proper secret boundaries.

7. Intentionally broken example — harmless goal bound too late

Suppose the requirement is: “generate build-marker.txt into target/classes before the JAR is assembled, so the marker must be inside the JAR.” The following execution is bound to the wrong phase:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-antrun-plugin</artifactId>
      <version>3.1.0</version>
      <executions>
        <execution>
          <id>write-build-marker</id>
          <!-- INTENTIONALLY WRONG FOR THIS DEMO -->
          <phase>package</phase>
          <goals><goal>run</goal></goals>
          <configuration>
            <target>
              <echo file="${project.build.outputDirectory}/build-marker.txt">marker-before-jar</echo>
            </target>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

For a JAR project, the packaging-provided jar:jar goal is already bound to package. Maven’s lifecycle documentation states that packaging bindings execute before POM-configured goals in the same phase. Therefore the JAR can be built before this custom antrun:run creates the marker.

./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository" clean package \
  | tee target/package-broken.log

jar tf target/lifecycle-lab-1.0.0.jar | grep build-marker \
  || echo "marker absent from jar"

grep -E '^\[INFO\] --- .*:(jar|run) ' target/package-broken.log

Least-destructive correction: move the custom generation to prepare-package, an earlier phase intended for work needed before packaging.

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-antrun-plugin</artifactId>
      <version>3.1.0</version>
      <executions>
        <execution>
          <id>write-build-marker</id>
          <!-- Correct: marker is generated before the package phase -->
          <phase>prepare-package</phase>
          <goals><goal>run</goal></goals>
          <configuration>
            <target>
              <echo file="${project.build.outputDirectory}/build-marker.txt">marker-before-jar</echo>
            </target>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository" clean package
jar tf target/lifecycle-lab-1.0.0.jar | grep build-marker.txt

The repair changes ordering at the model layer. It does not delete caches, disable tests, or invoke the JAR plugin twice. The original failed evidence remains available for comparison.

8. Performance without cargo-cult cache deletion

Separate timing domains: dependency/plugin download (cold repository), Maven model/configuration, resource processing, compilation/tests, and packaging. A warm isolated repository should reduce resolution time, but it should not change the effective POM or artifact semantics for immutable inputs. If a local repository entry is suspected, reproduce with a second new isolated lab repository before deleting anything from ~/.m2.

time ./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository-a" clean verify
time ./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository-a" clean verify

# Independent clean-room repository, not ~/.m2 deletion.
time ./mvnw -Dmaven.repo.local="$PWD/.lab-m2/repository-b" clean verify

Knowledge check

The log shows resources:resources (default-resources) and resources:resources (duplicate-resources). What should you inspect first?

A pom-packaging project produces no JAR at package. Is the JAR plugin necessarily broken?

A custom goal must generate a file that the JAR contains, but it is bound to the same package phase as jar:jar. What is the safer design?

Why is deleting ~/.m2/repository a poor first diagnostic step?

After install, where should you first look for the new project artifact in this lab?

Summary

Maven lifecycle incidents are model/order incidents until evidence proves otherwise. Preserve the log, verify Maven/JDK identity, inspect declared and effective POM state, read the executed goal sequence, then inspect generated/local-repository files. Repair the smallest causal binding, packaging, inheritance, or phase choice. Avoid destructive cache rituals and never confuse local install with remote deployment.

Next lesson

Prove the full lifecycle contract

Lesson 5 combines effective-model inspection, phase tracing, package/install comparison, prediction, and the wrong-phase repair into one checkpoint.

Official references and version notes

Version-sensitive statements in this lesson were checked against Apache Maven primary documentation on 2026-08-23. The production teaching baseline is Maven 3.9.16 through Maven Wrapper 3.3.4, running on JDK 21 and compiling the lab project with maven.compiler.release=17. Maven 4 is intentionally outside this chapter’s required path because it remains preview-stage. Default lifecycle plugin versions are properties of the Maven runtime and can change when the Maven runtime changes; re-check the matching core reference after an upgrade.

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.