Chapter 05Lesson 02~145 minutes

Maven POM Structure, Coordinates, Packaging, Build Lifecycle, Phases, and Goals: Guided Hands-On Workflow and Core Operations

Author and inspect a minimal Maven project while tracing validate, compile, test, package, verify, install, direct goals, and the target/local-repository evidence each operation creates.

pom.xmlLifecycle Tracetarget/Direct GoalsInstall

Learning objectives

  • Bootstrap a disposable Maven JAR project with production code, a resource, and one unit test.
  • Trace validate → compile → test → package → verify → install and map each command to generated evidence.
  • Contrast lifecycle-phase invocation with a version-pinned direct plugin-goal invocation.
  • Inspect the effective POM and JAR/local-repository outputs without relying on IDE state.
  • Choose the least powerful phase that matches a required outcome.
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. Scenario and safety boundary

You are onboarding a small service to CI. Before adding plugins or publishing anything, you need to prove exactly what Maven’s default JAR lifecycle does. The lab lives in a disposable directory, uses the project wrapper from Chapter 04, and redirects the Maven local repository to .lab-m2/repository. The normal ~/.m2/repository is neither deleted nor modified by cleanup.

Preflight: if your wrapper files differ from the Chapter 04 baseline, inspect and verify them first. Never replace wrapper URLs/checksums merely to make a download succeed.

2. Create the conventional project tree

mkdir -p lifecycle-lab/src/main/java/dev/academy
mkdir -p lifecycle-lab/src/main/resources
mkdir -p lifecycle-lab/src/test/java/dev/academy
cd lifecycle-lab

# Copy/retain the verified Maven Wrapper 3.3.4 files from the Chapter 04 lab:
#   mvnw  mvnw.cmd  .mvn/wrapper/maven-wrapper.properties
# The distribution must remain pinned to Maven 3.9.16 with the verified SHA-256.

mkdir -p .lab-m2/repository target/evidence

The directory names are Maven conventions inherited through the effective model. They are not magical JVM rules. src/main/java is main source input, src/main/resources is main resource input, src/test/java is test source input, and target is generated build state.

3. Author a minimal POM and application

<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</groupId>
  <artifactId>lifecycle-lab</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.13.4</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
package dev.academy;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public final class App {
    private App() {}

    public static String message() {
        Properties p = new Properties();
        try (InputStream in = App.class.getResourceAsStream("/build.properties")) {
            if (in == null) return "resource-missing";
            p.load(in);
            return p.getProperty("message", "missing");
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    public static void main(String[] args) {
        System.out.println(message());
    }
}
message=lifecycle-ok
package dev.academy;

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

class AppTest {
    @Test
    void readsPackagedResource() {
        assertEquals("lifecycle-ok", App.message());
    }
}

The single test dependency is present only so the test phase has observable work. Dependency scopes are examined in depth in Chapter 06; for now, treat test as “available to tests, not production runtime.”

4. Record identity and effective-model evidence before building

REPO="$PWD/.lab-m2/repository"
MVN="./mvnw -Dmaven.repo.local=$REPO"

$MVN --version | tee target/evidence/maven-version.txt
java -version 2>&1 | tee target/evidence/java-version.txt
sha256sum pom.xml src/main/java/dev/academy/App.java \
  src/main/resources/build.properties src/test/java/dev/academy/AppTest.java \
  | tee target/evidence/input-sha256.txt

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

MVN is only a shell convenience: every command still executes the project wrapper and the isolated repository. The effective-POM output is generated evidence, not a file to edit.

5. Walk the lifecycle and inspect causality

Command What Maven walks through Expected new evidence
$MVN validate Default lifecycle through validate. Often no target change in this minimal project because no default JAR goal is bound to validate.
$MVN compile Through resources + main compilation. target/classes/dev/academy/App.class and copied target/classes/build.properties.
$MVN test Through test resources, test compile, and Surefire test. target/test-classes and target/surefire-reports.
$MVN package Through package. target/lifecycle-lab-1.0.0.jar.
$MVN verify Through verify. Same JAR plus any explicitly bound verification/integration-test evidence; this minimal POM adds none.
$MVN install Through install. POM and JAR under isolated .lab-m2/repository/dev/academy/lifecycle-lab/1.0.0/.
$MVN clean
$MVN validate
find target -maxdepth 3 -type f 2>/dev/null | sort || true

$MVN compile
find target/classes -type f -print | sort

$MVN test
find target/surefire-reports -type f -print | sort

$MVN package
jar tf target/lifecycle-lab-1.0.0.jar | sort | tee target/evidence/jar-contents.txt
sha256sum target/lifecycle-lab-1.0.0.jar | tee target/evidence/package-sha256.txt

$MVN verify
$MVN install
find "$REPO/dev/academy/lifecycle-lab/1.0.0" -maxdepth 1 -type f -print | sort \
  | tee target/evidence/install-files.txt

6. Compare a direct goal with the compile phase

A direct goal invocation asks Maven to execute that goal; it does not automatically walk the earlier lifecycle phases. Use a clean build directory so the difference cannot be hidden by stale output.

$MVN clean

# Direct goal: compile Java, but do not ask the default lifecycle to process resources first.
$MVN org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile

test -f target/classes/dev/academy/App.class && echo "class: present"
test -f target/classes/build.properties || echo "resource: absent after direct compiler goal"

# Lifecycle phase: process-resources precedes compile.
$MVN compile
test -f target/classes/build.properties && echo "resource: present after compile phase"

The key evidence is not whether the direct goal “succeeds”; it is that its side effects differ from the phase because lifecycle prerequisites are not implied. In a production runbook, prefer a lifecycle phase when the desired outcome depends on the lifecycle contract.

7. Read the normal build log as an execution plan

$MVN clean package | tee target/evidence/package.log

grep -E '^\[INFO\] --- .*:[0-9].*:.* \(' target/evidence/package.log \
  | tee target/evidence/executed-goals.txt

Normal Maven output already prints plugin execution banners such as resources:...:resources (default-resources), compiler:...:compile (default-compile), surefire:...:test (default-test), and jar:...:jar (default-jar). That is concise evidence of which goals actually executed. You do not need to dump full -X logs for routine provenance.

8. package and install have different side effects

package creates the distributable artifact under target/. install first does everything through package/verify, then writes the project POM and artifact into the local repository using coordinates to determine the path. Install is not remote publication. Remote repository publication belongs to deploy and is not required in this lab.

rm -rf "$REPO/dev/academy/lifecycle-lab"
$MVN package

test -d "$REPO/dev/academy/lifecycle-lab" || echo "not installed after package"

$MVN install
find "$REPO/dev/academy/lifecycle-lab/1.0.0" -type f -maxdepth 1 -print | sort

9. Mini-challenge — choose the control from the outcome

Your CI stage needs compiled classes, resources, unit tests, the JAR, and any checks bound before verify, but it must not write the project into the local repository as an install target. Which command best expresses that contract?

Answer after predicting: ./mvnw verify. It walks through package and reaches verification without the later install side effect. If the project has no additional verify/integration bindings, package may produce the same artifact, but verify better expresses “run the build’s verification contract.”

10. Cleanup / rollback

cd ..
# Review the absolute disposable path before removal.
pwd
ls -la lifecycle-lab

# Remove only this lab when you no longer need the evidence.
rm -rf lifecycle-lab
Do not replace this with rm -rf ~/.m2. The lab intentionally isolated its repository so cleanup can be precise.

Knowledge check

After $MVN clean, you run the direct goal compiler:compile. Why might a main resource be absent from target/classes?

Which command creates the JAR but does not perform the project install side effect?

Why does validate sometimes appear to “do nothing”?

Where should you look to prove which plugin goals executed during a normal build?

Summary

You now have a causal map from phase request to generated evidence. Lifecycle phases execute all earlier phases in order; direct plugin goals do not imply lifecycle prerequisites. package creates the project artifact, verify reaches verification, and install additionally writes the artifact/POM to the local repository. Use an isolated repository and generated evidence so CI behavior is explainable without touching global caches.

Next lesson

Choose the right lifecycle design

Lesson 3 turns the mechanics into design decisions about phases, goals, packaging, default bindings, and parent conventions.

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.