rgoussu@goussu: ~/library/java/build-ecosystem
~/library/java/build-ecosystem cat maven.md

Maven deep dive

# Maven's POM model, fixed lifecycles, plugin-goal bindings, dependency mediation, BOMs, multi-module reactor, and the plugins worth knowing.

Conceptsaved 2026-08-09updated 2026-08-20 #java#build#tooling#maven

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.xml declares 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 (compilecompiler:compile, testsurefire:test, packagejar:jar, installinstall:install, deploydeploy: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-plugin exists and is rarely used), so JAVA_HOME is the real control. <release> sets the target — which language level and API surface javac accepts — 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 when JAVA_HOME is 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.cmd pin 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 only dependencyManagement, imported with scope=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's spring-boot-starter-parent is 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