Java Module 14 – Testing, build tools and code quality
Maven: POM, lifecycle and dependencies
Build a Java project with Maven: read each part of its POM, run the lifecycle phases, choose dependency scopes and fix plugin and compiler versions.
What you will learn
- Create a Maven project and explain each part of its POM
- Run lifecycle phases and choose the right scope for each dependency
- Set plugin versions, the compiler release and the Maven version so that every computer builds the same way
Before you start
What you’ll learn · Before you start
What you will learn
- Create a Maven project and explain each part of its POM
- Run lifecycle phases and choose the right scope for each dependency
- Set plugin versions, the compiler release and the Maven version so that every computer builds the same way
Before you start
On this page
Maven is a build tool for Java that works from one file, the POM (pom.xml, the Project Object Model). The POM
names the project with three coordinates, lists the libraries it depends on and says which plugins build it. Maven
then runs a fixed sequence of steps, the lifecycle: validate, compile, test, package, verify, install and deploy.
Asking for one phase runs every phase before it, so mvn package compiles the code, runs the tests and builds a JAR,
and stops at the first step that fails. Libraries come from Maven Central, a public repository, and are kept in a
local repository in your home folder, so a new computer builds the project with one command. Maven suits projects
that want the conventional layout that Java IDEs and continuous integration services understand without setup.
This lesson builds a small exercise pack, an invoice library with four JUnit tests, using Apache Maven 3.10.0 on Eclipse Temurin 25.0.4.1, and reads what Maven printed at each step.
The POM, part by part
The pack uses Maven’s standard layout: main code in src/main/java, tests in src/test/java, and everything the
build produces in target. Because that layout is the default, the POM never has to describe it.
<?xml version="1.0" encoding="UTF-8"?>
<!-- The exercise pack's POM: what the project is, what it needs and how it is built. -->
<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">
<!-- The version of the POM format itself: 4.0.0 for every Maven 3 project. -->
<modelVersion>4.0.0</modelVersion>
<!-- The coordinates: who publishes it, what it is called, which version. Together they name one artifact. -->
<groupId>com.example</groupId>
<artifactId>invoice-tools</artifactId>
<version>1.0.0</version>
<!-- What package builds: a JAR file. -->
<packaging>jar</packaging>
<properties>
<!-- javac release 25: Java 25 language rules, and only the APIs of Java SE 25. -->
<maven.compiler.release>25</maven.compiler.release>
<!-- Read source files as UTF-8 on every computer. -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- JUnit Jupiter, for the tests only: scope test keeps it out of the JAR's class path. -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- Exact versions of the plugins the lifecycle runs, so every Maven version builds the pack the same way. -->
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.2.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project> | Part | What it says in this POM |
|---|---|
modelVersion |
The version of the POM format, 4.0.0 for every Maven 3 project |
groupId, artifactId, version |
The coordinates com.example:invoice-tools:1.0.0, which name the artifact in every repository and every other POM |
packaging |
What the build makes, here a JAR, and so which plugin goals run in each phase |
properties |
Values that plugins read: maven.compiler.release becomes javac’s --release 25 |
dependencies |
Libraries by their coordinates and a scope: JUnit Jupiter 6.1.3, for the tests only |
pluginManagement |
The exact version of each plugin the lifecycle runs |
The file .mvn/maven.config beside the POM adds two options to every mvn command run in the project:
--batch-mode, which turns off colours and prompts, and --no-transfer-progress, which keeps download progress out
of the log.
The lifecycle runs its phases in order
The main phases of the default lifecycle for jar packaging, with the goal bound to each
Text description of the diagram
The diagram shows seven phases of Maven's default lifecycle in two columns, run in order. Asking Maven for a phase runs every phase before it first.
The first column, "Build the JAR", runs from top to bottom:
- validate checks that the POM is correct and complete.
- compile runs compiler:compile, which compiles src/main/java into target/classes.
- test runs surefire:test, which runs the tests and stops the build if one fails.
- package runs jar:jar, which builds the JAR in target.
An arrow leads from package to the top of the second column, "Check and share it":
- verify runs the checks you add to the POM, such as integration tests; a JAR project binds no goal to it by default.
- install copies the JAR and its POM into the local repository, ~/.m2/repository.
- deploy uploads them to a remote repository.
The full lifecycle has 23 phases. The ones not shown sit between these, such as process-resources, which copies resources before compile, and test-compile, which compiles the tests before test.
Each phase is a step of the build, and the work in it is done by plugin goals bound to it, written plugin:goal, such
as compiler:compile or surefire:test. A JAR project binds goals of the resources, compiler, Surefire, JAR, install
and deploy plugins; the other phases do nothing unless the POM adds a goal to them.
from session import run
# validate checks the POM and stops; package runs every phase up to package, and the JAR is listed; install copies
# the JAR and its POM into the local repository below the home folder.
run(
"""
mvn validate
mvn package
jar --list --file target/invoice-tools-1.0.0.jar
mvn -q install
ls ~/.m2/repository/com/example/invoice-tools/1.0.0
""",
folder="invoice-tools",
show=("java", "maven"),
) Output
# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 $ mvn validate [INFO] Scanning for projects... [INFO] [INFO] ---------------------< com.example:invoice-tools >---------------------- [INFO] Building invoice-tools 1.0.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ $ mvn package [INFO] Scanning for projects... [INFO] [INFO] ---------------------< com.example:invoice-tools >---------------------- [INFO] Building invoice-tools 1.0.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.5.0:resources (default-resources) @ invoice-tools --- [INFO] skip non existing resourceDirectory invoice-tools/src/main/resources [INFO] [INFO] --- compiler:3.16.0:compile (default-compile) @ invoice-tools --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 2 source files with javac [debug release 25] to target/classes [INFO] [INFO] --- resources:3.5.0:testResources (default-testResources) @ invoice-tools --- [INFO] skip non existing resourceDirectory invoice-tools/src/test/resources [INFO] [INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ invoice-tools --- [INFO] Recompiling the module because of changed dependency. [INFO] Compiling 1 source file with javac [debug release 25] to target/test-classes [INFO] [INFO] --- surefire:3.6.0:test (default-test) @ invoice-tools --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.example.invoice.InvoiceTest [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: … s -- in com.example.invoice.InvoiceTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- jar:3.5.1:jar (default-jar) @ invoice-tools --- [INFO] Building jar: invoice-tools/target/invoice-tools-1.0.0.jar [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ $ jar --list --file target/invoice-tools-1.0.0.jar META-INF/ META-INF/MANIFEST.MF com/ com/example/ com/example/invoice/ META-INF/maven/ META-INF/maven/com.example/ META-INF/maven/com.example/invoice-tools/ com/example/invoice/Invoice.class com/example/invoice/LineItem.class META-INF/maven/com.example/invoice-tools/pom.xml META-INF/maven/com.example/invoice-tools/pom.properties $ mvn -q install $ ls ~/.m2/repository/com/example/invoice-tools/1.0.0 _remote.repositories invoice-tools-1.0.0.jar invoice-tools-1.0.0.pom
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifecycle_session.py
Every line that starts with --- names one goal as it runs: the plugin, its version, the goal, the execution id and
the project. mvn validate checked the POM and ran no goal at all. mvn package ran six goals in order, compiled 2
main classes and 1 test class with release 25, ran 4 tests and built a JAR that holds the 2 classes and a copy of
the POM under META-INF/maven. mvn install ran the lifecycle again up to its install phase and copied the JAR and
its POM into ~/.m2/repository, where any other project on this computer can depend on
com.example:invoice-tools:1.0.0. Two more lifecycles exist: clean deletes target, and site builds a project
website.
Dependency scopes
A dependency’s scope says which class paths it joins and whether projects that depend on yours receive it too:
- compile, the default, is on every class path and passes to the projects that depend on yours.
- provided is used to compile and test, but the JDK or a container such as a servlet engine supplies it at run time.
- runtime is not needed to compile, only to test and run, as with a JDBC driver.
- test is only for compiling and running the tests, as JUnit is here.
- system names a JAR file on your disk instead of a repository, which Maven’s guide advises against.
- import copies the managed versions of a bill of materials (a BOM) into
dependencyManagement.
Settings that make every build the same
Three settings decide whether the pack builds the same way on every computer:
- Plugin versions. A POM that names none gets the defaults of whichever Maven version runs it.
- The release.
maven.compiler.releasemakes javac check the language and the APIs of that Java release. - Maven itself. The Maven Wrapper, the
mvnwscript with its.mvn/wrapper/maven-wrapper.properties, downloads and runs the Maven version that the project names.
Check yourself
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Phases and goals in depth: what runs, what stops a build, what skips tests Pro: opens with a Pro, Premium or Ultimate pass
A phase is a position in the lifecycle, and a goal is the work a plugin does there. Knowing which of the two a command names explains most surprises in a Maven log.
In this part:
- The 23 phases of the default lifecycle, and the 8 that run a goal here
- A goal runs alone; a phase runs everything before it
- A failing test stops the build, reproduced
3 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Dependency scopes measured: the tree, the class paths and a provided-scope failure Pro: opens with a Pro, Premium or Ultimate pass
Scopes decide what is on the class path when code compiles, when tests run and when the program runs. This part reads a real dependency tree, then reproduces the failure a wrong scope causes in production.
In this part:
- Reading the tree of a project with two dependencies
- Which class path each scope joins
- A provided dependency missing at run time, reproduced
- How scopes travel through the tree
2 runnable examples (Java, Python) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Fixed versions that keep builds the same: failures reproduced, and a broken pack to repair Pro: opens with a Pro, Premium or Ultimate pass
Each version you fix prevents one kind of build that works on one computer and breaks on another. This part reproduces each failure, then gives you a broken pack to repair.
In this part:
- The same POM, two Maven versions, different plugins
- Release 21 against source and target 21, reproduced on Java 21
- Exercise: repair the broken pack
5 runnable examples (Python, XML) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
-
Fresher · Warm-up
What is the difference between a Maven phase and a goal?
A phase is a step of a lifecycle, such as compile, test or package, and a goal is a task a plugin performs, written plugin:goal, such as compiler:compile or surefire:test. A phase does nothing by itself: it runs the goals bound to it, which come from the packaging and from the plugins configured in the POM. Asking for a phase runs every earlier phase of its lifecycle first, so mvn package also compiles and tests. Asking for a goal runs only that goal, outside the lifecycle: mvn surefire:test on a fresh checkout finds no compiled tests, because nothing compiled them. Goals that do not belong to the build, such as dependency:tree or help:describe, are always called directly.
-
Fresher to mid
A Maven build passes on your laptop but fails in CI on the same commit. The CI image was updated last week and nobody changed the POM. How do you find out what changed, and how do you stop it from happening again?
-
Fresher to mid
A service is built with JDK 25 but runs on servers with Java 21. The POM sets maven.compiler.source and target to 21. It builds and starts, then a request fails with NoClassDefFoundError: java/lang/IO. Explain what happened and which setting prevents it.
-
Mid to senior
A command-line tool's tests all pass in CI, yet the tool fails on its first run in production with NoClassDefFoundError for a class of a JSON library. The POM declares that library with scope provided. Explain the failure, fix it, and say how you would catch the next one before release.
Key takeaways
- The POM names the project by three coordinates, lists its dependencies with a scope each and fixes its plugins.
- Asking for a phase runs every phase before it:
mvn packageran six goals and 4 tests before it built the JAR. - The scope decides the class paths: JUnit stays in test, a servlet API is provided and a JDBC driver is runtime.
- Plugin versions, the compiler release and the Maven version are three separate settings, and each needs a value.
References
- Maven Getting Started Guide (The Apache Software Foundation)
- Introduction to the Build Lifecycle (The Apache Software Foundation)
- Introduction to the Dependency Mechanism (The Apache Software Foundation)
- POM Reference (The Apache Software Foundation)
- Maven Wrapper (The Apache Software Foundation)
- Apache Maven Compiler Plugin: Setting the --release of the Java Compiler (The Apache Software Foundation)
- Apache Maven Compiler Plugin: compiler:compile parameters (The Apache Software Foundation)
- Maven Core 3.9.11: Default Lifecycle Bindings (The Apache Software Foundation)
- Download Apache Maven (The Apache Software Foundation)
- Configuring Apache Maven (.mvn/maven.config) (The Apache Software Foundation)
- Maven Surefire Plugin: Skipping Tests (The Apache Software Foundation)
- Apache Maven Dependency Plugin: dependency:tree and dependency:build-classpath (The Apache Software Foundation)
- Apache Maven Enforcer: Require Plugin Versions (The Apache Software Foundation)
- Apache Maven Help Plugin: help:describe (The Apache Software Foundation)
- JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe (OpenJDK)
- JEP 512: Compact Source Files and Instance Main Methods (OpenJDK)
- The javac Command (Java SE 25): --release (Oracle)
- The Java Virtual Machine Specification, Java SE 25: 4.1 The ClassFile Structure (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress