ZetCode

Maven build phases

last modified November 8, 2026

In this article we explain Maven build lifecycles and phases, how phases are connected to plugin goals, and how to add our own goals to a build. The examples use Maven 3.10 and Java 25.

Lifecycles, phases, and goals

A Maven build is organized in three layers. A lifecycle is an ordered sequence of phases. A phase is a named step, such as compile or package; it does no work on its own. The work is done by plugin goals, which are bound to phases. For example, the compile goal of the compiler plugin is bound to the compile phase.

When we run a phase, Maven runs every phase of the same lifecycle up to and including that phase, and for each of them it executes the bound goals. This gives every project the same predictable build: mvn package always compiles and tests the code before it is packaged.

Maven has three built-in lifecycles:

The example project

We use a small project with one class and one test.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>com.example</groupId>
    <artifactId>phases</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>6.1.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

The POM does not configure any plugins. Maven still knows how to compile, test, and package the code, because each packaging type (here the default jar) comes with a set of default plugin bindings. Since no plugin versions are pinned, Maven 3.10 uses its built-in default versions.

src/main/java/com/example/Calculator.java
package com.example;

public class Calculator {

    public static int add(int a, int b) {
        return a + b;
    }
}

The class we compile and package.

src/test/java/com/example/CalculatorTest.java
package com.example;

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

import org.junit.jupiter.api.Test;

class CalculatorTest {

    @Test
    void addsNumbers() {
        assertEquals(5, Calculator.add(2, 3));
    }
}

The JUnit test that runs in the test phase.

Default lifecycle phases

The help:describe goal of the Help plugin shows the phases of a lifecycle and the goals bound to them. With -Dcmd=deploy it describes the deploy phase, the last phase of the default lifecycle, and lists all 23 phases with the goals bound for the jar packaging. The plugin versions are the Maven defaults; the Help plugin does not take versions pinned in the POM into account.

$ mvn help:describe -Dcmd=deploy
[INFO] --- help:3.5.2:describe (default-cli) @ phases ---
[INFO] 'deploy' is a phase corresponding to this plugin:
org.apache.maven.plugins:maven-deploy-plugin:3.2.0:deploy

It is a part of the lifecycle for the POM packaging 'jar'. This lifecycle includes the following phases:
* validate: Not defined
* initialize: Not defined
* generate-sources: Not defined
* process-sources: Not defined
* generate-resources: Not defined
* process-resources: org.apache.maven.plugins:maven-resources-plugin:3.5.0:resources
* compile: org.apache.maven.plugins:maven-compiler-plugin:3.16.0:compile
* process-classes: Not defined
* generate-test-sources: Not defined
* process-test-sources: Not defined
* generate-test-resources: Not defined
* process-test-resources: org.apache.maven.plugins:maven-resources-plugin:3.5.0:testResources
* test-compile: org.apache.maven.plugins:maven-compiler-plugin:3.16.0:testCompile
* process-test-classes: Not defined
* test: org.apache.maven.plugins:maven-surefire-plugin:3.6.0:test
* prepare-package: Not defined
* package: org.apache.maven.plugins:maven-jar-plugin:3.5.1:jar
* pre-integration-test: Not defined
* integration-test: Not defined
* post-integration-test: Not defined
* verify: Not defined
* install: org.apache.maven.plugins:maven-install-plugin:3.2.0:install
* deploy: org.apache.maven.plugins:maven-deploy-plugin:3.2.0:deploy

...

The most important phases

Only a few of the 23 phases are used directly; the others serve as hooks where plugins can attach their goals.

Phase names with a pre-, post-, or process- prefix, and the generate-* phases, are not normally run from the command line. They exist so that plugins can generate sources, copy resources, or prepare environments at the right moment.

Phase execution order

Running mvn package runs all phases from validate to package. The lines starting with --- show the goals that were executed, in order: resources are copied, the main and test sources are compiled, the tests run, and finally the JAR is built. Each goal shows the plugin prefix, its version, the goal name, and in parentheses the execution ID; default-compile and similar IDs mark the default bindings. We show only the goal lines and a few result lines of the output.

$ mvn package
[INFO] --- resources:3.5.0:resources (default-resources) @ phases ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ phases ---
[INFO] --- resources:3.5.0:testResources (default-testResources) @ phases ---
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ phases ---
[INFO] --- surefire:3.6.0:test (default-test) @ phases ---
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.040 s -- in com.example.CalculatorTest
[INFO] --- jar:3.5.1:jar (default-jar) @ phases ---
[INFO] Building jar: /home/user/phases/target/phases-1.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
...

The clean lifecycle

The clean lifecycle has three phases: pre-clean, clean, and post-clean. The clean goal of the clean plugin, bound to the clean phase, deletes the target directory.

Phases from different lifecycles can be combined in one command. The common mvn clean package first runs the clean lifecycle up to clean and then the default lifecycle up to package, so nothing from a previous build can leak into the new one.

$ ls target
classes
generated-sources
generated-test-sources
maven-archiver
maven-status
phases-1.0-SNAPSHOT.jar
surefire-reports
test-classes
$ mvn clean
[INFO] --- clean:3.5.0:clean (default-clean) @ phases ---
[INFO] Deleting /home/user/phases/target
[INFO] BUILD SUCCESS
...
$ ls
pom.xml
src

Running goals directly

Besides phases, we can run individual plugin goals with the prefix:goal syntax, for example mvn dependency:tree. A goal run this way does not trigger any lifecycle phases. Here the clean phase deletes the compiled classes, and the Surefire test goal then finds nothing to run, because the test sources were not compiled. Goals run from the command line get the default-cli execution ID.

$ mvn clean surefire:test
[INFO] --- clean:3.5.0:clean (default-clean) @ phases ---
[INFO] --- surefire:3.6.0:test (default-cli) @ phases ---
[INFO] No tests to run.
[INFO] BUILD SUCCESS
...

Skipping tests

The skipTests property compiles the tests but does not run them. The maven.test.skip property also skips the compilation of test sources and the copying of test resources. Skipping tests is useful for a quick local build, but a build that is shared with others should run them.

$ mvn clean package -DskipTests
[INFO] --- compiler:3.16.0:compile (default-compile) @ phases ---
[INFO] Compiling 1 source file with javac [debug release 25] to target/classes
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ phases ---
[INFO] Compiling 1 source file with javac [debug release 25] to target/test-classes
[INFO] --- surefire:3.6.0:test (default-test) @ phases ---
[INFO] Tests are skipped.
...
$ mvn clean package -Dmaven.test.skip=true
[INFO] --- compiler:3.16.0:compile (default-compile) @ phases ---
[INFO] Compiling 1 source file with javac [debug release 25] to target/classes
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ phases ---
[INFO] Not compiling test sources
[INFO] --- surefire:3.6.0:test (default-test) @ phases ---
[INFO] Tests are skipped.
...

Binding goals to phases

Plugins declared in the POM can bind their goals to any phase. In the following project, the AntRun plugin prints a message in the process-sources phase, before compilation, and another one in the package phase, after the JAR is created.

pom.xml (build section)
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-antrun-plugin</artifactId>
            <version>3.2.0</version>
            <executions>
                <execution>
                    <id>before-compile</id>
                    <phase>process-sources</phase>
                    <goals>
                        <goal>run</goal>
                    </goals>
                    <configuration>
                        <target>
                            <echo level="info" message="Sources are ready for compilation"/>
                        </target>
                    </configuration>
                </execution>
                <execution>
                    <id>after-package</id>
                    <phase>package</phase>
                    <goals>
                        <goal>run</goal>
                    </goals>
                    <configuration>
                        <target>
                            <echo level="info" message="Created ${project.build.finalName}.jar"/>
                        </target>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Each execution has its own ID, the phase it binds to, the goals to run, and its configuration. When a plugin's goals have a default phase, the phase element can be omitted; the run goal of AntRun has none, so the phase must be given.

<echo level="info" message="Created ${project.build.finalName}.jar"/>

The Ant echo task logs at the warning level by default, which Maven would show as [WARNING]; the level attribute lowers it to info. Maven properties such as ${project.build.finalName} are expanded inside the target.

Goals bound to the same phase run in the order in which they appear in the POM, after the goals of the default bindings. So the after-package execution runs after the JAR plugin.

$ mvn package
[INFO] --- antrun:3.2.0:run (before-compile) @ antrun ---
[INFO]      [echo] Sources are ready for compilation
[INFO] --- resources:3.5.0:resources (default-resources) @ antrun ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ antrun ---
[INFO] --- resources:3.5.0:testResources (default-testResources) @ antrun ---
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ antrun ---
[INFO] --- surefire:3.6.0:test (default-test) @ antrun ---
[INFO] --- jar:3.5.1:jar (default-jar) @ antrun ---
[INFO] --- antrun:3.2.0:run (after-package) @ antrun ---
[INFO]      [echo] Created antrun-1.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
...

Overriding default executions

To change the configuration of a default binding, we use its execution ID, such as default-compile or default-testCompile. An execution with a new ID would not replace the default execution; it would run the goal a second time.

pom.xml (compiler plugin)
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.16.0</version>
    <configuration>
        <compilerArgs>
            <arg>-Xlint:all</arg>
        </compilerArgs>
    </configuration>
    <executions>
        <execution>
            <id>default-testCompile</id>
            <configuration>
                <compilerArgs>
                    <arg>-Xlint:none</arg>
                </compilerArgs>
            </configuration>
        </execution>
    </executions>
</plugin>

The plugin-level configuration enables all lint warnings. The default-testCompile execution overrides the compiler arguments only for the test sources; the default-compile execution for the main sources keeps -Xlint:all. A compilerArgs list in an execution replaces the plugin-level list instead of being merged with it.

Best practices

Source

Introduction to the build lifecycle

In this article we have explained Maven build lifecycles and phases and shown how to bind plugin goals to phases.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.