Overview
Maven is the convention-over-configuration build tool: a declarative XML POM describes what the project is, and a fixed lifecycle decides how it is built. Its rigidity is the feature — every Maven project has the same layout, the same phases, and the same commands, which is why it remains the default for libraries and conservative teams, and why its repository format (Maven Central) became the substrate the whole JVM ecosystem — Gradle included — resolves against.
Key points
- POM model:
pom.xmldeclares GAV coordinates (groupId:artifactId:version),packaging(jar/war/pom/…),properties, dependencies, and plugin configuration. Effective POM = your POM + inherited parents + the Super POM defaults. - Fixed lifecycles: three of them —
clean,default(the build),site. A lifecycle is an ordered list of phases; invoking a phase runs every phase before it. - Phase → plugin goal binding: phases do nothing themselves; the packaging type binds
plugin goals to phases (
compile→compiler:compile,test→surefire:test,package→jar:jar,install→install:install,deploy→deploy:deploy). - Plugins are the unit of everything: compiling, testing, packaging, releasing — all plugin goals. Maven core is just the lifecycle engine plus dependency resolution.
- Maven compiles with whatever JDK runs it. There is no widely-used equivalent of
Gradle's toolchain provisioning (
maven-toolchains-pluginexists and is rarely used), soJAVA_HOMEis the real control.<release>sets the target — which language level and API surfacejavacaccepts — but the compiler itself is the one in the running JDK, and a<release>newer than that JDK supports simply fails. The consequence worth remembering: nothing warns you whenJAVA_HOMEis not the JDK you meant — the build just uses it. - Dependency mediation: nearest-wins — the version closest to the root of the
dependency tree is selected; ties go to declaration order. No Gradle-style
highest-version conflict resolution; override deliberately via
dependencyManagement. - Scopes:
compile(default, everywhere),provided(compile only, container supplies it),runtime(not on the compile classpath),test,import(BOMs only),system(avoid). - Multi-module reactor: an aggregator POM lists
<modules>; Maven computes the inter-module build order and builds them in one invocation. - Wrapper:
mvnw/mvnw.cmdpin the Maven version in the repo — commit them. - Maven 4 in one line: a modernised core — cleaner POM (build vs consumer POM split), better multi-module ergonomics — while staying compatible with the existing model.
The default lifecycle's phase chain with its bindings — invoking any phase runs every phase before it, so ./mvnw verify compiles, tests, and packages first:
flowchart LR
C["compile - compiler:compile"] --> T["test - surefire:test"]
T --> P["package - jar:jar"]
P --> IT["integration-test - failsafe"]
IT --> V["verify - failsafe"]
V --> I["install - install:install"]
I --> Dep["deploy - deploy:deploy"]
Details
dependencyManagement and BOMs
<dependencyManagement>declares versions (and scopes/exclusions) without adding dependencies; child modules then depend version-less and inherit the managed version.- A BOM (bill of materials) is a
pom-packaged artifact containing onlydependencyManagement, imported withscope=import— how Spring Boot (spring-boot-dependencies), Quarkus (quarkus-bom), and JUnit (junit-bom) keep a whole dependency universe version-aligned in one line.
Parent POMs and profiles
- A
<parent>gives inheritance: plugin management, properties, dependencyManagement flow down. Corporate parent POMs centralise build policy; Spring Boot'sspring-boot-starter-parentis the same mechanism. - Profiles (
<profiles>) conditionally add configuration — activated by-P, JDK version, OS, or property. Use sparingly: profile-dependent artifacts undermine "same input, same output".
Repositories and publishing
- Resolution order: local repository (
~/.m2/repository) → declared remotes → Maven Central. Private repositories (Nexus, Artifactory) proxy Central and host internal artifacts; credentials live in~/.m2/settings.xml, never in the POM. - Publishing to Central requires GAV coordinates under a verified namespace, sources + javadoc jars, and GPG signatures; deployment now goes through the Central publishing portal (successor to OSSRH).
Plugins worth knowing
| Plugin | Why it matters |
|---|---|
maven-enforcer-plugin |
Fails the build on rule violations: banned dependencies, dependency convergence, required Maven/JDK versions |
maven-shade-plugin |
Builds fat jars; can relocate (shade) packages to avoid dependency clashes |
maven-surefire-plugin |
Runs unit tests in the test phase |
maven-failsafe-plugin |
Runs integration tests (*IT) in integration-test/verify, so a failing IT still runs cleanup |
maven-compiler-plugin |
Sets <release> (target Java version) and compiler args |
Examples
Minimal 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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.rgoussu</groupId>
<artifactId>demo-app</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Then: ./mvnw verify builds and tests; ./mvnw dependency:tree explains mediation
decisions.
Related
- The Java build ecosystem — parent summary: where Maven sits among the three tiers.
- Gradle deep dive — the flexible alternative; contrast task DAG vs fixed lifecycle.
- Testing in Java — surefire/failsafe are the Maven ends of the unit/integration split.
- Version managers — how the JDK
that
JAVA_HOMEpoints at gets there in the first place.