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:
default— builds, tests, packages, and deploys the projectclean— removes files generated by previous buildssite— generates project documentation and reports
The example project
We use a small project with one class and one test.
<?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.
package com.example;
public class Calculator {
public static int add(int a, int b) {
return a + b;
}
}
The class we compile and package.
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.
validate— checks that the project is correct and all necessary information is availablecompile— compiles the main sources totarget/classestest— runs the unit testspackage— packages the compiled code, for example into a JAR fileverify— runs checks on the package, such as integration testsinstall— copies the package to the local repository (~/.m2/repository)deploy— uploads the package to a remote repository
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.
<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.
<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
- Run
mvn clean verifybefore pushing changes: it builds from scratch and runs all tests and checks without installing anything. - Use
mvn installonly when another local project needs the artifact as a dependency. - Bind custom goals to the earliest phase where their input is ready, for
example code generators to
generate-sources. - Pin plugin versions in
pluginManagement, so the build does not change when a newer Maven release comes with different default versions.
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
List all Java tutorials.