Maven Lifecycle Phases

The 23 phases of the default lifecycle, and how plugin goals bind to them

← Back to Index

What is a Maven Lifecycle?

A lifecycle is a fixed, ordered sequence of named phases. Phases don't do anything by themselves — they're just labelled checkpoints. What actually happens during a phase is whatever plugin goals are bound to it. Maven ships three independent lifecycles: default (build and deploy), clean (remove build output), and site (generate documentation).

The problem this solves: every Java project needs roughly the same sequence of steps — compile, test, package — but different projects need different tools to do each step. The lifecycle gives every project the same named checkpoints; plugins decide what actually runs at each one. This is why mvn test means the same conceptual thing whether your project uses JUnit 4, JUnit 5, or TestNG — the phase is universal, the tool behind it isn't.

The default Lifecycle — Full Phase List

23 phases in fixed order. Running any phase executes every phase before it automatically. In practice you'll name maybe 6 of these directly; the rest exist as hooks for plugins to bind custom goals to.

validate                  # project structure is correct
initialize                # set up build state, e.g. create directories
generate-sources          # generate any source code to be compiled (codegen)
process-sources           # e.g. filter/template source files
generate-resources        # generate resources for inclusion in the package
process-resources         # copy resources to target/classes
compile                   # ★ compile src/main/java → target/classes
process-classes           # post-process compiled bytecode (e.g. bytecode enhancement)
generate-test-sources     # generate test source code
process-test-sources      # process test source files
generate-test-resources   # generate test resources
process-test-resources    # copy test resources
test-compile              # compile src/test/java
process-test-classes      # post-process compiled test classes
test                       # ★ run unit tests (Surefire)
prepare-package           # pre-packaging hook (e.g. Spring Boot repackage groundwork)
package                   # ★ bundle into jar/war in target/
pre-integration-test      # set up environment for integration tests (start containers, etc.)
integration-test          # run integration tests (Failsafe)
post-integration-test     # tear down integration test environment
verify                    # ★ run checks to validate the package is correct
install                   # ★ copy artifact to ~/.m2/repository
deploy                    # ★ upload artifact to a remote repository

★ marks the phases you'll actually type as commands daily — everything else is invoked implicitly because it runs before whichever ★ phase you called.

Why so many "invisible" phases exist

Phases like generate-sources or process-test-classes are rarely named directly — they exist so plugins have a guaranteed, well-defined slot to hook into without clashing with each other. Code generators (Lombok's annotation processing, Protobuf compilation, OpenAPI client generation) bind to generate-sources specifically so their output is ready before the real compile phase runs.

The clean and site Lifecycles

These are entirely separate from default — they don't share phases, and running one doesn't trigger the other (which is why mvn clean install explicitly names both).

// clean lifecycle — 3 phases
pre-clean   // hook before cleaning
clean       // ★ deletes target/
post-clean  // hook after cleaning

// site lifecycle — 4 phases, generates project documentation/reports
pre-site
site         // ★ generates HTML docs into target/site/
post-site
site-deploy  // uploads the generated site somewhere

mvn clean package    // runs: pre-clean → clean → post-clean, THEN validate → ... → package
mvn site:run          // serves the generated site locally for preview

Binding Goals to Phases

A goal is a single unit of work a plugin can perform — e.g. compiler:compile, surefire:test. Each phase has default goal bindings for standard packaging types, and you can bind additional goals to any phase explicitly.

Default bindings for jar packaging

process-resources       → resources:resources
compile                 → compiler:compile
process-test-resources  → resources:testResources
test-compile            → compiler:testCompile
test                    → surefire:test
package                 → jar:jar
install                 → install:install
deploy                  → deploy:deploy

// This is WHY 'mvn test' runs Surefire without you configuring anything —
// the binding already exists for jar packaging by default.

Binding a custom goal — example: enforce code style before anything builds

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-checkstyle-plugin</artifactId>
    <executions>
        <execution>
            <phase>validate</phase>  <!-- runs FIRST, fails fast before compiling -->
            <goals><goal>check</goal></goals>
        </execution>
    </executions>
</plugin>

// Run a goal directly, bypassing the lifecycle entirely (no preceding phases)
// — useful for one-off checks, not for a real build
mvn checkstyle:check
mvn compiler:compile      // compiles WITHOUT running validate/initialize first
Phase invocation vs goal invocation

mvn package invokes a phase — every phase before it runs too. mvn compiler:compile invokes a goal directly — only that goal runs, nothing else. Use phase invocation for actual builds; use direct goal invocation only for quick, isolated checks where you deliberately don't want the full lifecycle overhead.

Skipping Phases and Plugin Executions

# Test-related skips — different scope, choose carefully
mvn package -DskipTests              # compiles tests, doesn't RUN them
mvn package -Dmaven.test.skip=true   # doesn't even compile them — faster, less safe

# Skip a specific quality-gate plugin without disabling tests
mvn package -Dcheckstyle.skip=true
mvn package -Dspotbugs.skip=true

# Skip in the POM itself — avoid this for CI; it silently disables
# the check for EVERYONE, not just your local run
<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration><skip>true</skip></configuration>
</plugin>

Interview Questions

🎓 Junior level

Q: What happens when you run mvn package?
Maven executes every phase of the default lifecycle in order, up to and including package: validate, compile, test, package. It doesn't skip ahead — phases always run in their fixed sequence.

Q: What is the difference between a phase and a goal?
A phase is a named checkpoint in the lifecycle (e.g. compile) — it does nothing by itself. A goal is the actual unit of work a plugin performs (e.g. compiler:compile). Phases have goals bound to them; running a phase executes whatever goals are bound to it and every phase before it.

Q: Are clean, default, and site related?
No — they're three completely independent lifecycles that don't share phases. Running mvn clean only triggers the pre-clean → clean → post-clean sequence; it has no effect on default phases unless you explicitly chain them, like mvn clean install.

🔥 Senior level

Q: Why do code generators (Lombok, Protobuf, OpenAPI generators) typically bind to generate-sources rather than some other phase?
generate-sources runs before compile but after initialize — it's specifically designed as the phase where source code that doesn't exist yet gets created, guaranteeing it's on disk and ready before the compiler runs. Binding a generator any later (e.g. to process-sources or beyond) risks generated classes not being visible to the compiler in time; binding earlier risks build state not being initialised yet.

Q: When would you invoke a goal directly instead of a phase?
When you want exactly one unit of work with no lifecycle overhead — for example, mvn dependency:tree to inspect dependencies without triggering a full compile, or mvn checkstyle:check as a quick local style check during development. Direct goal invocation is for diagnostics and quick checks; real builds should always go through phase invocation so the full, correctly-ordered lifecycle runs.

Q: How would you make a build fail fast on a code-quality violation before wasting time compiling and testing?
Bind the quality plugin's check goal to an early phase — validate or initialize — rather than its default binding (which for many quality plugins is later, around verify). Failing at validate means a broken style rule is caught in seconds, before the much more expensive compile and test phases run at all. This is a real CI optimisation: cheap checks first, expensive checks last.