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.
Learning objectives
- Bootstrap a disposable Maven JAR project with production code, a resource, and one unit test.
-
Trace
validate → compile → test → package → verify → installand 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.
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.
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
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?
Because a direct compiler goal does not automatically execute
the process-resources lifecycle phase. Invoking the
compile phase would walk through
process-resources first.
Which command creates the JAR but does not perform the project
install side effect?
package (or verify, which also remains
before install).
Why does validate sometimes appear to “do
nothing”?
A lifecycle phase may have zero goals bound in the effective model. It is still an ordered checkpoint and can gain work when plugins are bound to it.
Where should you look to prove which plugin goals executed during a normal build?
The normal Maven build log prints plugin execution banners; retain a concise filtered copy together with effective-model and artifact evidence.
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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.