Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 22 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 22 minutes free
Free: The POM, part by part · The lifecycle runs its phases in order · Dependency scopes · 2 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 15 minutes.

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

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.

invoice-tools/pom.xml XML · invoice-tools/pom.xml
<?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.

XSD Validator Paste a POM and Maven's schema, maven-4.0.0.xsd, to find a misspelt or misplaced element with its line number.

The lifecycle runs its phases in order

Maven default lifecycle for a JAR in two columns: validate, compile, test and package, then verify, install and deploy.Build the JARCheck and share itvalidatechecks the POMcompilecompiler:compiletestsurefire:testpackagejar:jarverifyyour own checksinstall~/.m2/repositorydeployremote repository

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:

  1. validate checks that the POM is correct and complete.
  2. compile runs compiler:compile, which compiles src/main/java into target/classes.
  3. test runs surefire:test, which runs the tests and stops the build if one fails.
  4. 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":

  1. verify runs the checks you add to the POM, such as integration tests; a JAR project binds no goal to it by default.
  2. install copies the JAR and its POM into the local repository, ~/.m2/repository.
  3. 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.

validate, package and install on the exercise pack Python · lifecycle_session.py
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.release makes javac check the language and the APIs of that Java release.
  • Maven itself. The Maven Wrapper, the mvnw script 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.

  1. Question 1 of 4 You run mvn package on a JAR project. Which phases of the default lifecycle run?

    Choose one answer.

    Show the answer to question 1

    Answer: Every phase from validate up to and including package, in order

    Asking for a phase runs every phase before it. In the recorded run, mvn package ran the resources, compiler, Surefire and JAR goals in that order, and stopped after package: nothing was installed or deployed.

  2. Question 2 of 4 In the recorded run of mvn package on the exercise pack, how many plugin goals ran? Count the lines that start with ---.

    Type a number.

    Show the answer to question 2

    Answer: 6 goals

    resources:resources, compiler:compile, resources:testResources, compiler:testCompile, surefire:test and jar:jar: six goals, bound to six of the phases between validate and package.

  3. Question 3 of 4 Your code uses only the java.sql interfaces, and a JDBC driver must be present when the program runs. Which scope fits the driver?

    Choose one answer.

    Show the answer to question 3

    Answer: runtime

    runtime keeps the driver off the compile class path, so no code can depend on its classes by accident, and puts it on the class paths used to test and run the program. provided would leave it out at run time, and test would leave it out everywhere but the tests.

  4. Question 4 of 4 The POM names no plugin versions and builds fine on your laptop. What can still go wrong on another computer?

    Choose one answer.

    Show the answer to question 4

    Answer: Another Maven version runs other default plugin versions on the same POM

    Without versions, each Maven version uses the plugin versions it binds by default, so the same commit is built by different plugins on a laptop and a build server. pluginManagement fixes the versions, and the Maven Wrapper fixes the Maven version itself.

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.

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.

  1. 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.

  2. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

  4. 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.

    Model answer, Pro. First step and follow-ups

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 package ran 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.