Maven Refresher
Apache Maven build tool — project management, dependencies, and lifecycle quick reference
Table of Contents
Overview & Philosophy
Apache Maven is a build automation and project management tool for Java (and JVM) projects. Released in 2004, it introduced two ideas that changed how Java projects are structured: convention over configuration and a declarative Project Object Model (POM). Instead of scripting every build step (like Ant), you declare what your project is and Maven figures out how to build it.
Core Principles
- Convention over Configuration — standard directory layout, standard lifecycle phases. Follow conventions and most config is zero.
- Declarative POM —
pom.xmldescribes the project; Maven executes the implied build plan. - Dependency Management — Central Repository automatically downloads and caches JARs. Transitive deps resolved automatically.
- Plugin Architecture — every action (compile, test, package) is performed by a plugin. Maven itself is just a plugin executor.
- Reproducible Builds — same POM + same repo = same artifact, anywhere, any machine.
Maven vs Gradle vs Ant
| Feature | Maven | Gradle | Ant |
|---|---|---|---|
| Build file | pom.xml (XML) |
build.gradle (Groovy/Kotlin DSL) |
build.xml (XML) |
| Model | Declarative | Declarative + imperative | Fully imperative |
| Convention | Strong (opinionated) | Moderate | None |
| Dependency management | Built-in (Central) | Built-in (Central + custom) | Manual (Ivy optional) |
| Build speed | Moderate (parallel with -T) | Fast (incremental, build cache) | Fast (no overhead) |
| Multi-module | First-class (reactor) | First-class (composite builds) | Manual |
| IDE support | Excellent (IDEA, Eclipse) | Excellent | Good |
| Ecosystem maturity | Very mature (2004) | Mature (2012) | Legacy (2000) |
| Spring Boot default | Yes (initializr default) | Optional | No |
Installation
Maven requires a JDK on the PATH. Install via package manager or download directly.
# macOS via Homebrew
brew install maven
# Ubuntu/Debian
sudo apt install maven
# Verify installation
mvn --version
# Apache Maven 3.9.6
# Java version: 21.0.2, vendor: Eclipse Adoptium
# Manual install: download, unzip, add to PATH
export MAVEN_HOME=/opt/apache-maven-3.9.6
export PATH=$MAVEN_HOME/bin:$PATH
Maven Wrapper (mvnw)
The Maven Wrapper pins a specific Maven version to the project, so every developer and CI machine uses the exact same version without a system-wide installation.
# Add wrapper to an existing project
mvn wrapper:wrapper
# or pin a specific version
mvn wrapper:wrapper -Dmaven=3.9.6
# Wrapper files committed to git
.mvn/wrapper/maven-wrapper.properties
.mvn/wrapper/maven-wrapper.jar # bootstrap JAR (optional in newer versions)
mvnw # Unix shell script
mvnw.cmd # Windows batch script
# Use wrapper instead of system maven
./mvnw clean install
./mvnw -pl my-module test
mvnw, mvnw.cmd, and .mvn/wrapper/ to version control.
This ensures reproducible builds across all environments without requiring Maven to be pre-installed.
POM Basics
The Project Object Model (pom.xml) is the fundamental unit of Maven.
It describes the project's identity, dependencies, build configuration, and metadata.
Every Maven project has exactly one POM file at its root.
Minimal POM
<?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>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
</project>
GAV Coordinates
Every artifact in Maven is uniquely identified by three coordinates — together called GAV (GroupId, ArtifactId, Version).
| Coordinate | Purpose | Convention | Example |
|---|---|---|---|
groupId |
Organization or project namespace | Reverse domain name | com.google.guava |
artifactId |
Module name (no spaces) | Lowercase, hyphens | guava |
version |
Release version | SemVer; SNAPSHOT for dev | 32.1.3-jre |
-SNAPSHOT signals a development build. Maven re-downloads
SNAPSHOT artifacts periodically (daily by default) to pick up the latest. Release versions
are immutable once deployed — Maven caches them forever.
Packaging Types
| Packaging | Output | Use case |
|---|---|---|
jar | .jar | Library or executable (default) |
war | .war | Web application (servlet container) |
ear | .ear | Enterprise application (JEE) |
pom | POM only | Parent POM or BOM — no compiled artifact |
maven-plugin | .jar | Maven plugin artifact |
<!-- War packaging for a web app -->
<packaging>war</packaging>
<!-- Pom packaging for a parent/BOM -->
<packaging>pom</packaging>
Parent POM
A child POM can inherit configuration from a parent POM. The parent is declared with
the <parent> element. All settings in the parent (dependencies,
plugins, properties) are inherited unless overridden.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<!-- relativePath empty = always resolve from repo, not local filesystem -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>my-spring-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
Super POM & Effective POM
Every POM implicitly inherits from the Super POM — Maven's built-in default POM that defines the standard directory layout, Central Repository, and default plugin versions. The effective POM is the fully-merged result of your POM + parent chain + Super POM.
# View the fully merged effective POM (great for debugging)
mvn help:effective-pom
# Pretty-print to a file
mvn help:effective-pom -Doutput=effective-pom.xml
Full POM Structure
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<!-- Identity -->
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>My Application</name>
<description>A sample Maven project</description>
<url>https://example.com/my-app</url>
<!-- Properties (variables) -->
<properties>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- Dependencies -->
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
</dependencies>
<!-- Build configuration -->
<build>
<plugins>
<!-- plugins go here -->
</plugins>
</build>
</project>
Project Structure
Maven's standard directory layout eliminates the need to configure source directories, resource directories, or test directories. Follow the convention and Maven knows exactly where everything is.
my-project/
├── pom.xml # Project descriptor
├── mvnw # Maven wrapper (Unix)
├── mvnw.cmd # Maven wrapper (Windows)
├── .mvn/
│ └── wrapper/
│ └── maven-wrapper.properties
│
├── src/
│ ├── main/
│ │ ├── java/ # Application source code
│ │ │ └── com/example/
│ │ │ └── App.java
│ │ ├── resources/ # Non-Java resources (properties, XML, templates)
│ │ │ └── application.properties
│ │ └── webapp/ # Web resources (WAR projects only)
│ │ ├── WEB-INF/
│ │ │ └── web.xml
│ │ └── index.html
│ │
│ └── test/
│ ├── java/ # Test source code (mirrors main/java structure)
│ │ └── com/example/
│ │ └── AppTest.java
│ └── resources/ # Test-only resources
│ └── test-data.json
│
└── target/ # Generated by Maven (never commit this)
├── classes/ # Compiled main classes
├── test-classes/ # Compiled test classes
├── my-app-1.0.0-SNAPSHOT.jar # Packaged artifact
├── surefire-reports/ # Unit test reports
└── site/ # Generated site (mvn site)
target/ directory contains all generated artifacts, compiled classes,
and test reports. It is always regenerated by Maven and must never be committed to version control.
Overriding Directories
You can override default directories in the POM, but only do this when you genuinely cannot follow the convention (e.g., migrating a legacy project).
<build>
<!-- Override source directory (not recommended unless migrating) -->
<sourceDirectory>src/java</sourceDirectory>
<testSourceDirectory>test/java</testSourceDirectory>
<resources>
<resource>
<directory>src/conf</directory>
</resource>
</resources>
<!-- Override output directory -->
<outputDirectory>bin/classes</outputDirectory>
</build>
Build Lifecycle
Maven defines three built-in lifecycles. Each lifecycle is an ordered sequence of phases. When you run a phase, Maven runs all phases that come before it in sequence.
The Three Lifecycles
| Lifecycle | Purpose | Trigger |
|---|---|---|
| default | Compile, test, package, deploy | mvn compile, mvn install, etc. |
| clean | Delete generated files | mvn clean |
| site | Generate project documentation | mvn site |
Default Lifecycle Phases
These are the most commonly used phases in the default lifecycle, in execution order. Running any phase executes all prior phases first.
| Phase | Description | Bound Plugin (default JAR) |
|---|---|---|
validate |
Validate project structure and POM is correct | — |
initialize |
Initialize build state (set properties, create dirs) | — |
generate-sources |
Generate source code (e.g., JAXB, protobuf) | — |
process-sources |
Filter and process source files | resources:resources |
compile |
Compile main source code into classes | compiler:compile |
process-classes |
Post-process compiled classes (e.g., bytecode enhancement) | — |
generate-test-sources |
Generate test source code | — |
test-compile |
Compile test source code | compiler:testCompile |
test |
Run unit tests (Surefire) | surefire:test |
package |
Package compiled code into distributable format (JAR/WAR) | jar:jar or war:war |
pre-integration-test |
Set up environment for integration tests | — |
integration-test |
Run integration tests (Failsafe) | failsafe:integration-test |
post-integration-test |
Tear down integration test environment | — |
verify |
Check integration test results, quality gates | failsafe:verify |
install |
Install artifact to local ~/.m2 repository |
install:install |
deploy |
Deploy artifact to remote repository | deploy:deploy |
Common Commands by Phase
# Run tests (also runs validate, compile, test-compile first)
mvn test
# Package into JAR/WAR (also runs compile, test)
mvn package
# Install to local ~/.m2 cache (most common for library development)
mvn install
# Install but skip tests (faster for development iteration)
mvn install -DskipTests
# Deploy to remote repository (CI/CD final step)
mvn deploy
# Clean generated files, then build fresh
mvn clean install
# Clean lifecycle only
mvn clean
# Generate project site
mvn site
Clean Lifecycle Phases
| Phase | Description |
|---|---|
pre-clean | Executes before project cleaning |
clean | Deletes the target/ directory |
post-clean | Executes after project cleaning |
mvn install runs: validate → initialize → generate-sources → process-sources →
compile → test-compile → test → package → verify → install. You never need to chain them manually.
Just name the phase you want to end at.
Dependencies
Basic Dependency Declaration
<dependencies>
<!-- Compile-time dependency (default scope) -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<!-- Test dependency -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
<!-- Servlet API: provided by container at runtime -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<!-- JDBC driver: only needed at runtime, not compile time -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Dependency Scopes
| Scope | Compile CP | Test CP | Runtime CP | Packaged | Transitive | Use case |
|---|---|---|---|---|---|---|
compile |
Yes | Yes | Yes | Yes | Yes | Default. Needed everywhere. |
provided |
Yes | Yes | No | No | No | Container provides at runtime (Servlet API, Lombok) |
runtime |
No | Yes | Yes | Yes | Yes | Not needed to compile but needed to run (JDBC drivers) |
test |
No | Yes | No | No | No | Test frameworks only (JUnit, Mockito, AssertJ) |
system |
Yes | Yes | Yes | No | No | Local filesystem JAR. Avoid — use a local repo instead. |
import |
N/A | N/A | N/A | N/A | N/A | Only in <dependencyManagement>. Imports a BOM. |
Transitive Dependencies
When you declare a dependency, Maven also pulls in all of that dependency's
compile and runtime scoped dependencies automatically. This is
transitive dependency resolution.
# Visualize the full dependency tree
mvn dependency:tree
# Filter to a specific artifact
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
# Analyze for unused declared and used undeclared deps
mvn dependency:analyze
Dependency Mediation (Nearest Wins)
When two transitive dependency paths pull in different versions of the same library, Maven uses the nearest wins rule: whichever version is closest to your project root in the dependency tree wins.
# Example conflict: your-app depends on:
# A → C:1.0
# B → C:2.0
# C:1.0 wins because A appears before B (depth 1 either way, first declaration wins)
# Resolution: explicitly declare the version you want in your POM
# Your explicit declaration is always at depth 1 — nearest
Excluding Transitive Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>3.2.2</version>
<exclusions>
<!-- Exclude Logback, we want Log4j2 instead -->
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
Optional Dependencies
Mark a dependency as optional to indicate it should not be transitively
pulled into downstream projects. Used by libraries that support multiple implementations
(e.g., Spring's optional support for various serialization libraries).
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
<optional>true</optional>
</dependency>
BOM (Bill of Materials)
A BOM is a POM with <packaging>pom</packaging> that centrally
defines a curated, compatible set of dependency versions. Downstream projects import the
BOM with scope=import and get all versions for free, without having to specify
each one individually.
<dependencyManagement>
<dependencies>
<!-- Import Spring Boot BOM — no version needed on any spring-boot-* dep -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- No version needed — BOM provides it -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
Version Ranges
<!-- Prefer exact versions in production. Version ranges are available: -->
<version>[1.0,2.0)</version> <!-- >= 1.0 and < 2.0 -->
<version>[1.5,]</version> <!-- >= 1.5, any upper bound -->
<version>(,1.0]</version> <!-- <= 1.0 -->
<version>[1.2]</version> <!-- Exactly 1.2 -->
mvn versions:display-dependency-updates.
Dependency Management
dependencyManagement vs dependencies
| Element | Effect | When to use |
|---|---|---|
<dependencies> |
Adds the dependency to the project's classpath immediately | Actual dependencies your code uses |
<dependencyManagement> |
Declares version/scope defaults only — does NOT add to classpath | Parent POMs and BOMs to centralize version control |
<!-- Parent POM: centralize versions -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Child POM: no version needed, inherited from parent's dependencyManagement -->
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<!-- Version comes from parent's dependencyManagement -->
</dependency>
</dependencies>
Version Properties Pattern
Using properties as version placeholders makes it trivial to update all related artifacts at once. This is the standard approach in enterprise multi-module projects.
<properties>
<java.version>21</java.version>
<spring-boot.version>3.2.2</spring-boot.version>
<jackson.version>2.16.1</jackson.version>
<junit.version>5.10.1</junit.version>
<mockito.version>5.8.0</mockito.version>
<testcontainers.version>1.19.3</testcontainers.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
Spring Boot Parent POM Pattern
<!-- Option 1: Inherit from spring-boot-starter-parent -->
<!-- Gives you: version management, compiler config, resource filtering -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<relativePath/>
</parent>
<!-- Option 2: Import BOM without inheriting (use when you have your own parent) -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
import scope is only valid inside <dependencyManagement>.
You cannot use scope=import in a regular <dependencies> block.
Also, you can import multiple BOMs — they are merged in declaration order.
Plugins
Every action Maven takes is performed by a plugin. The lifecycle phases are just hooks that invoke plugin goals. Understanding plugins is understanding Maven's actual power.
Plugin Coordinates
Plugins are also Maven artifacts, identified by GAV coordinates. Goals are the individual
tasks a plugin performs. The fully-qualified form is:
groupId:artifactId:version:goal
# Run a specific plugin goal directly
mvn org.apache.maven.plugins:maven-compiler-plugin:3.12.1:compile
# Short form (works for official Apache plugins)
mvn compiler:compile
# Run a goal not bound to any lifecycle phase
mvn dependency:tree
mvn help:effective-pom
mvn versions:display-dependency-updates
Plugin Configuration
<build>
<plugins>
<!-- Compiler plugin: set Java version -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
<!-- Enable preview features -->
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
<!-- Surefire: unit test runner -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<!-- Run tests in parallel -->
<parallel>methods</parallel>
<threadCount>4</threadCount>
<!-- Include/exclude test patterns -->
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
<include>**/*Spec.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
Plugin Executions
You can bind a plugin goal to a specific lifecycle phase using
<executions>. This is how you add custom steps to the build.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>print-build-info</id>
<!-- Bind to the validate phase -->
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<echo>Building ${project.artifactId} version ${project.version}</echo>
</target>
</configuration>
</execution>
</executions>
</plugin>
Common Plugins Reference
| Plugin | Goal(s) | Purpose |
|---|---|---|
maven-compiler-plugin |
compile, testCompile |
Compile Java sources; set Java version |
maven-surefire-plugin |
test |
Run unit tests (JUnit 4/5, TestNG) |
maven-failsafe-plugin |
integration-test, verify |
Run integration tests (separate from unit tests) |
maven-jar-plugin |
jar |
Package JAR, configure manifest (Main-Class) |
maven-shade-plugin |
shade |
Create fat/uber JAR with all deps bundled |
maven-assembly-plugin |
single |
Create distribution archives (zip, tar.gz) |
maven-war-plugin |
war |
Package WAR for servlet containers |
exec-maven-plugin |
java, exec |
Run Java main class or external programs |
maven-enforcer-plugin |
enforce |
Enforce rules (Java version, dep convergence, banned deps) |
versions-maven-plugin |
display-dependency-updates, set |
Check for newer versions; update version numbers |
maven-release-plugin |
prepare, perform |
Automate release process (tag, version bump, deploy) |
jacoco-maven-plugin |
prepare-agent, report |
Code coverage measurement and reporting |
spring-boot-maven-plugin |
repackage, run |
Create executable Spring Boot JAR; run app |
maven-resources-plugin |
resources |
Copy and filter resource files |
maven-dependency-plugin |
tree, analyze, copy-dependencies |
Dependency analysis and manipulation |
Executable JAR with maven-shade-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.Main</mainClass>
</transformer>
</transformers>
<!-- Exclude cryptographic signature files from deps -->
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
Enforcer Plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>enforce</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireMavenVersion>
<version>[3.8,)</version>
</requireMavenVersion>
<requireJavaVersion>
<version>[21,)</version>
</requireJavaVersion>
<!-- Fail if two deps declare different versions of the same transitive dep -->
<dependencyConvergence/>
<bannedDependencies>
<excludes>
<exclude>commons-logging:commons-logging</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
pluginManagement
Just like <dependencyManagement> for dependencies,
<pluginManagement> centralizes plugin version and configuration
in a parent POM without actually executing those plugins.
<!-- Parent POM -->
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<!-- Child POM: just reference the plugin, version and config inherited -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!-- No version or configuration needed -->
</plugin>
</plugins>
</build>
Profiles
Profiles allow you to customize build behavior for different environments (dev, test, production) or conditions (OS, JDK version). A profile can override any section of the POM.
Profile Declaration
<profiles>
<!-- Development profile: skip tests, use dev DB -->
<profile>
<id>dev</id>
<activation>
<!-- Active by default when no other profile is specified -->
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<db.url>jdbc:postgresql://localhost:5432/myapp_dev</db.url>
<log.level>DEBUG</log.level>
</properties>
</profile>
<!-- Production profile -->
<profile>
<id>prod</id>
<properties>
<db.url>jdbc:postgresql://prod-db:5432/myapp</db.url>
<log.level>WARN</log.level>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<goals><goal>enforce</goal></goals>
<configuration>
<rules><dependencyConvergence/></rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Profile Activation Types
<!-- Activate by system property: mvn install -Denv=staging -->
<activation>
<property>
<name>env</name>
<value>staging</value>
</property>
</activation>
<!-- Activate by OS -->
<activation>
<os>
<name>Windows 10</name>
<family>windows</family>
<arch>amd64</arch>
</os>
</activation>
<!-- Activate by JDK version -->
<activation>
<jdk>[17,)</jdk>
</activation>
<!-- Activate when a file exists -->
<activation>
<file>
<exists>src/main/resources/production.properties</exists>
</file>
</activation>
Activating Profiles on the Command Line
# Activate a specific profile
mvn install -Pprod
# Activate multiple profiles
mvn install -Pprod,metrics
# Deactivate a profile that would otherwise be active
mvn install -P!dev
# List active profiles for the current build
mvn help:active-profiles
Settings.xml Profiles
Profiles in ~/.m2/settings.xml apply to all projects on the machine.
Useful for machine-specific configuration like repository credentials or proxy settings.
<!-- ~/.m2/settings.xml -->
<settings>
<profiles>
<profile>
<id>my-nexus</id>
<repositories>
<repository>
<id>nexus</id>
<url>https://nexus.example.com/repository/maven-public/</url>
</repository>
</repositories>
</profile>
</profiles>
<!-- Always activate this profile -->
<activeProfiles>
<activeProfile>my-nexus</activeProfile>
</activeProfiles>
</settings>
Multi-Module Projects
Maven's reactor builds multiple modules as a single unit, resolving inter-module dependencies and determining build order automatically. Multi-module is the standard structure for any non-trivial Java application.
Typical Multi-Module Layout
my-project/ # Root (parent) module
├── pom.xml # Parent POM: packaging=pom, lists modules
├── my-api/ # Module 1: API contracts/interfaces
│ ├── pom.xml
│ └── src/
├── my-core/ # Module 2: Business logic
│ ├── pom.xml
│ └── src/
├── my-web/ # Module 3: Web layer (depends on core + api)
│ ├── pom.xml
│ └── src/
└── my-integration-tests/ # Module 4: Integration tests
├── pom.xml
└── src/
Parent POM (Root)
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0-SNAPSHOT</version>
<!-- Must be pom for parent/aggregator -->
<packaging>pom</packaging>
<name>My Project (Parent)</name>
<!-- List of child modules (directory names) -->
<modules>
<module>my-api</module>
<module>my-core</module>
<module>my-web</module>
<module>my-integration-tests</module>
</modules>
<properties>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- Centralize all dependency versions here -->
<dependencyManagement>
<dependencies>
<!-- Internal modules: reference by GAV -->
<dependency>
<groupId>com.example</groupId>
<artifactId>my-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>my-core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Child Module POM
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<!-- Reference parent -->
<parent>
<groupId>com.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0-SNAPSHOT</version>
<!-- relativePath: ../pom.xml is the default, can omit -->
<relativePath>../pom.xml</relativePath>
</parent>
<!-- Only artifactId is needed — groupId and version inherited from parent -->
<artifactId>my-web</artifactId>
<name>My Web Module</name>
<dependencies>
<!-- Depend on sibling module -->
<dependency>
<groupId>com.example</groupId>
<artifactId>my-core</artifactId>
<!-- Version managed by parent's dependencyManagement -->
</dependency>
</dependencies>
</project>
Reactor Build Commands
# Build all modules (from project root)
mvn clean install
# Build only a specific module and its dependencies (-am = also make)
mvn install -pl my-web -am
# Build a specific module and modules that depend on it (-amd = also make dependents)
mvn install -pl my-core -amd
# Build multiple specific modules
mvn install -pl my-api,my-core
# Skip a module
mvn install -pl !my-integration-tests
# Build in parallel (N threads or C * CPU count)
mvn install -T 4
mvn install -T 1C
# Resume from a specific module after a failure
mvn install -rf my-web
<modules>. If my-web depends on my-core,
my-core will always be built first regardless of declaration order.
Repositories
Maven Central
Maven Central (https://repo.maven.apache.org/maven2/) is the default public
repository. It is declared in the Super POM so all Maven projects can resolve artifacts
from it without any configuration.
Repository Types
| Type | Description | Typical use |
|---|---|---|
Local (~/.m2/repository) |
Disk cache on developer machine | Fast local resolution; result of mvn install |
| Remote (Central) | Public internet repository | Open source libraries |
| Remote (private) | Corporate repository manager | Internal artifacts, security-approved OSS |
Declaring Additional Repositories
<repositories>
<!-- Sonatype OSSRH (snapshots of popular projects) -->
<repository>
<id>sonatype-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<!-- Spring Milestones -->
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!-- Plugin repositories (separate from dependency repositories) -->
<pluginRepositories>
<pluginRepository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</pluginRepository>
</pluginRepositories>
Repository Managers (Nexus / Artifactory)
In enterprise environments, a repository manager sits between developers and the internet. All artifact requests go through it — it caches external artifacts and hosts internal ones.
<!-- settings.xml: mirror all requests through corporate Nexus -->
<mirrors>
<mirror>
<id>nexus-mirror</id>
<name>Corporate Nexus</name>
<url>https://nexus.example.com/repository/maven-public/</url>
<!-- * means mirror ALL repositories -->
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
<!-- settings.xml: credentials for private repos -->
<servers>
<server>
<id>nexus-releases</id>
<username>deploy-user</username>
<password>${env.NEXUS_PASSWORD}</password>
</server>
</servers>
Deploying Artifacts
<!-- pom.xml: distribution management -->
<distributionManagement>
<repository>
<id>nexus-releases</id>
<url>https://nexus.example.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<url>https://nexus.example.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
# Deploy to remote repository (requires distributionManagement and server credentials)
mvn deploy
# Deploy without running tests
mvn deploy -DskipTests
settings.xml, not
pom.xml. The pom.xml is committed to source control — the
settings.xml lives on the machine or in CI secrets. Reference env vars with
${env.SECRET_NAME}.
Properties
Maven properties are key-value pairs used as variables throughout the POM using
the ${property.name} syntax.
Property Categories
| Category | Prefix | Examples |
|---|---|---|
| Project properties | project.* |
${project.version}, ${project.groupId}, ${project.build.directory} |
| Settings properties | settings.* |
${settings.localRepository} |
| Environment variables | env.* |
${env.JAVA_HOME}, ${env.CI} |
| System properties | (none) | ${java.version}, ${os.name} |
| User-defined | (none) | ${my.custom.property} |
User-Defined Properties
<properties>
<!-- Java version -->
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<!-- Encoding (always set this) -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Library versions -->
<guava.version>32.1.3-jre</guava.version>
<lombok.version>1.18.30</lombok.version>
<!-- Custom build properties -->
<skip.integration.tests>false</skip.integration.tests>
</properties>
Resource Filtering
Enable resource filtering to substitute property values into resource files at build time.
This is how you inject build-time configuration (version numbers, environment) into
application.properties.
<!-- Enable filtering in pom.xml -->
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
# src/main/resources/application.properties
app.name=${project.name}
app.version=${project.version}
app.build.time=${maven.build.timestamp}
<resource> block without filtering for binary resources.
Overriding Properties on the Command Line
# Override any property at the command line with -D
mvn install -Djava.version=17
mvn install -Dskip.integration.tests=true
mvn test -Dtest=UserServiceTest # Run a specific test class
Common Commands
Lifecycle Commands
| Command | Description |
|---|---|
mvn validate | Validate POM and project structure |
mvn compile | Compile main sources |
mvn test-compile | Compile test sources |
mvn test | Compile and run unit tests |
mvn package | Compile, test, and package (JAR/WAR) |
mvn verify | Run all verifications including integration tests |
mvn install | Build and install to local ~/.m2 |
mvn deploy | Build and deploy to remote repository |
mvn clean | Delete target/ directory |
mvn site | Generate project site documentation |
Common Flags
| Flag | Description |
|---|---|
-DskipTests | Skip test execution (still compiles tests) |
-Dmaven.test.skip=true | Skip test compilation AND execution |
-pl module-name | Build specific module(s) only |
-am | Also build modules required by -pl targets |
-amd | Also build modules depending on -pl targets |
-T 4 | Build with 4 parallel threads |
-T 1C | One thread per CPU core |
-o | Offline mode (use only local cache, no downloads) |
-U | Force update SNAPSHOTs from remote repos |
-e | Show exception stack traces on error |
-X | Debug mode (very verbose) |
-q | Quiet mode (only show warnings and errors) |
-P profile-id | Activate a profile |
-rf :module | Resume reactor build from specified module |
-N | Non-recursive (root module only, ignore child modules) |
Plugin Goal Commands
| Command | Description |
|---|---|
mvn dependency:tree | Print full dependency tree |
mvn dependency:analyze | Find unused declared / used undeclared deps |
mvn dependency:resolve | Resolve and list all deps |
mvn dependency:copy-dependencies | Copy all deps to target/dependency/ |
mvn versions:display-dependency-updates | Show available dependency version upgrades |
mvn versions:display-plugin-updates | Show available plugin version upgrades |
mvn versions:set -DnewVersion=2.0.0 | Update version in all POMs at once |
mvn help:effective-pom | Print the merged effective POM |
mvn help:active-profiles | List active profiles |
mvn help:describe -Dplugin=compiler | Describe a plugin's goals and parameters |
mvn exec:java -Dmainclass=com.example.Main | Run a Java main class |
Real-World Command Chains
# Full clean build with tests
mvn clean verify
# Fast iteration: skip tests, build specific module
mvn install -DskipTests -pl my-web -am
# CI pipeline: clean build, run all tests, publish artifacts
mvn clean deploy -Pprod -T 1C
# Run a single test class
mvn test -Dtest=UserServiceTest
# Run a single test method
mvn test -Dtest=UserServiceTest#shouldCreateUser
# Check for outdated dependencies
mvn versions:display-dependency-updates -DprocessParent=true
# Update all versions interactively
mvn versions:set -DnewVersion=2.0.0-SNAPSHOT
mvn versions:commit # or versions:revert to undo
# Generate and view site
mvn site site:run # Starts local HTTP server at localhost:8080
Integration with IDEs
IntelliJ IDEA
IntelliJ has first-class Maven support built in. No plugin installation required.
- Import: File → Open → select
pom.xml→ "Open as Project" - Maven tool window: View → Tool Windows → Maven. Shows lifecycle phases and plugin goals as a tree.
- Reload POM: Click the refresh icon in the Maven tool window after editing
pom.xml. Or enable auto-import. - Run phase: Double-click any phase in the Maven tool window, or right-click
pom.xml→ Maven menu. - Create project: File → New Project → Maven Archetype.
Eclipse / STS (Spring Tool Suite)
Eclipse uses the m2eclipse plugin (built into STS and Eclipse IDE for Enterprise Java).
- Import: File → Import → Maven → Existing Maven Projects → select root directory
- Update project: Right-click project → Maven → Update Project (Alt+F5)
- Run build: Right-click
pom.xml→ Run As → Maven build... → specify goals - Effective POM: Right-click
pom.xml→ Maven → Show effective POM
pom.xml, always trigger a Maven refresh/update in your IDE.
In IntelliJ, use Ctrl+Shift+O (or the elephant icon).
In Eclipse, use Alt+F5. Without this, your IDE's classpath and
auto-complete will be out of sync with what Maven actually builds.
Best Practices
Version Management
- Pin all versions. Never omit a version and rely on Maven to resolve it — you lose control of what gets built.
- Use a BOM for sets of libraries that must stay in sync (e.g., all Jackson modules, all Spring modules).
- Centralize versions in
<properties>in the parent POM so you only change one line to upgrade a library. - Run
mvn versions:display-dependency-updatesregularly to stay on top of security patches.
Reproducible Builds
<!-- In properties, lock down build metadata for reproducibility -->
<properties>
<project.build.outputTimestamp>2024-01-01T00:00:00Z</project.build.outputTimestamp>
</properties>
<!-- Avoid SNAPSHOT dependencies in release builds -->
<!-- Use the enforcer plugin to ban them -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>no-snapshots-in-release</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireReleaseDeps>
<!-- Only enforce when building a release (non-SNAPSHOT) -->
<onlyWhenRelease>true</onlyWhenRelease>
</requireReleaseDeps>
</rules>
</configuration>
</execution>
</executions>
</plugin>
CI/CD Integration
# GitHub Actions example
name: Maven Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven # Cache ~/.m2 between runs
- name: Build and test
run: ./mvnw clean verify -Pprod -T 1C
- name: Deploy
if: github.ref == 'refs/heads/main'
run: ./mvnw deploy -DskipTests -Pprod
env:
NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
Code Quality
<!-- JaCoCo coverage gate: fail if below 80% -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>check</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
General Guidelines
- Always use the Maven Wrapper (
mvnw) — no system Maven required. - Set
maven.compiler.sourceandtargetexplicitly — never rely on defaults. - Set
project.build.sourceEncodingtoUTF-8— default is platform-dependent. - Use
mvn verifynotmvn packagein CI —verifyruns integration tests and quality gates. - Separate unit tests (Surefire) from integration tests (Failsafe) — integration tests are slow and environment-dependent.
- Use
pluginManagementin parent POMs to centralize plugin versions across modules.
Common Pitfalls
Dependency Hell and Version Conflicts
mvn dependency:tree,
find the conflicting versions, and explicitly declare the version you need in your POM.
# Diagnose version conflicts
mvn dependency:tree -Dverbose | grep "omitted for conflict"
# Show why a specific artifact was included
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
Scope Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Using compile for a test library |
Test framework (JUnit/Mockito) bundled into production artifact | Add <scope>test</scope> |
Using compile for a JDBC driver |
Driver JAR bundled unnecessarily; vendor lock-in visible at compile time | Change to <scope>runtime</scope> |
Forgetting provided for Servlet API in WAR |
ClassCastException in container: two versions of Servlet API on classpath | Add <scope>provided</scope> |
Using system scope |
Non-portable builds, Jenkins breaks, developers need exact file path | Install the JAR to a local repo or use a repository manager |
SNAPSHOT vs Release Confusion
requireReleaseDeps rule in your release profile.
Slow Builds
- No parallelism: Add
-T 1Cfor multi-module builds. - Re-downloading artifacts: Check network/proxy config; ensure
~/.m2is cached in CI. - Integration tests in unit test phase: Name integration tests
*IT.javaand use Failsafe, not Surefire. - Slow test suite: Use
-DskipTestsduring development iteration; run full suite only on CI. - Downloading SNAPSHOTs every time: Use
-ofor offline mode once you have all deps.
.m2 Cache Corruption
# Symptom: bizarre resolution errors, "corrupt artifact", or "invalid POM"
# Fix: delete the corrupted artifact and re-download
rm -rf ~/.m2/repository/com/example/my-library/
# Nuclear option: delete entire local cache and start fresh
# (slow — Maven re-downloads everything)
rm -rf ~/.m2/repository/
# Check for partially downloaded files (*.lastUpdated = failed download)
find ~/.m2/repository -name "*.lastUpdated" -delete
Transitive Dependency Surprises
# You added library A, but suddenly you have library B's transitive deps
# contaminating your classpath
# Step 1: see what's actually on the classpath
mvn dependency:tree
# Step 2: analyze for problems
mvn dependency:analyze
# Step 3: exclude the transitive dep you don't want
# (then explicitly add it if you need a different version)
<dependency>
<groupId>some.library</groupId>
<artifactId>library-a</artifactId>
<version>2.0</version>
<exclusions>
<!-- Exclude a transitive dep that conflicts with our own -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
The Duplicate Class Problem (Fat JARs)
META-INF/services/*
and Spring's spring.factories must be merged, not overwritten. Without
proper transformers, only the last copy of each file survives and features disappear silently.
Always configure ServicesResourceTransformer and
AppendingTransformer when shading.
Build Order Issues in Multi-Module Projects
# Problem: module B depends on module A, but A hasn't been installed yet
# (happens when running mvn from a child module directly)
# Wrong: run from child directory (doesn't build A first)
cd my-web && mvn compile # Fails: my-core not in local repo
# Right: run from root with -am (also make dependencies)
mvn compile -pl my-web -am
# Or: install all modules first, then iterate
mvn install -DskipTests # from root
cd my-web && mvn compile # Now works — my-core is in ~/.m2