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 POMpom.xml describes 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
When to choose Maven
Maven is the right choice when you need a well-understood, stable build tool with maximum IDE integration, an enormous plugin ecosystem, and strict enforcement of conventions. It dominates enterprise Java and is the default for Spring Boot projects. Choose Gradle when you need faster incremental builds or more flexible scripting.

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
Best practice: always commit the wrapper
Commit 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).

CoordinatePurposeConventionExample
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 versions
A version ending in -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

PackagingOutputUse case
jar.jarLibrary or executable (default)
war.warWeb application (servlet container)
ear.earEnterprise application (JEE)
pomPOM onlyParent POM or BOM — no compiled artifact
maven-plugin.jarMaven 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)
Always add target/ to .gitignore
The 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

LifecyclePurposeTrigger
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.

PhaseDescriptionBound 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

PhaseDescription
pre-cleanExecutes before project cleaning
cleanDeletes the target/ directory
post-cleanExecutes after project cleaning
Phase ordering is the key insight
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 -->
Avoid version ranges in production
Version ranges cause non-reproducible builds — a build today may pull a different JAR than a build tomorrow. Always pin to exact versions. Use the Versions plugin to check for updates: mvn versions:display-dependency-updates.

Dependency Management

dependencyManagement vs dependencies

ElementEffectWhen 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>
BOM import scope limitation
The 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

PluginGoal(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
Reactor build order
Maven determines build order from inter-module dependencies, not from the order listed in <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

TypeDescriptionTypical 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
Never put credentials in pom.xml
Repository credentials (username/password) must live in 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

CategoryPrefixExamples
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}
Filtering and binary resources
Never enable filtering on directories containing binary files (images, fonts, keystore files). Filtering processes files as text and will corrupt binary content. Use a separate <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

CommandDescription
mvn validateValidate POM and project structure
mvn compileCompile main sources
mvn test-compileCompile test sources
mvn testCompile and run unit tests
mvn packageCompile, test, and package (JAR/WAR)
mvn verifyRun all verifications including integration tests
mvn installBuild and install to local ~/.m2
mvn deployBuild and deploy to remote repository
mvn cleanDelete target/ directory
mvn siteGenerate project site documentation

Common Flags

FlagDescription
-DskipTestsSkip test execution (still compiles tests)
-Dmaven.test.skip=trueSkip test compilation AND execution
-pl module-nameBuild specific module(s) only
-amAlso build modules required by -pl targets
-amdAlso build modules depending on -pl targets
-T 4Build with 4 parallel threads
-T 1COne thread per CPU core
-oOffline mode (use only local cache, no downloads)
-UForce update SNAPSHOTs from remote repos
-eShow exception stack traces on error
-XDebug mode (very verbose)
-qQuiet mode (only show warnings and errors)
-P profile-idActivate a profile
-rf :moduleResume reactor build from specified module
-NNon-recursive (root module only, ignore child modules)

Plugin Goal Commands

CommandDescription
mvn dependency:treePrint full dependency tree
mvn dependency:analyzeFind unused declared / used undeclared deps
mvn dependency:resolveResolve and list all deps
mvn dependency:copy-dependenciesCopy all deps to target/dependency/
mvn versions:display-dependency-updatesShow available dependency version upgrades
mvn versions:display-plugin-updatesShow available plugin version upgrades
mvn versions:set -DnewVersion=2.0.0Update version in all POMs at once
mvn help:effective-pomPrint the merged effective POM
mvn help:active-profilesList active profiles
mvn help:describe -Dplugin=compilerDescribe a plugin's goals and parameters
mvn exec:java -Dmainclass=com.example.MainRun 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
Force IDE to re-sync after dependency changes
After editing 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-updates regularly 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.source and target explicitly — never rely on defaults.
  • Set project.build.sourceEncoding to UTF-8 — default is platform-dependent.
  • Use mvn verify not mvn package in CI — verify runs integration tests and quality gates.
  • Separate unit tests (Surefire) from integration tests (Failsafe) — integration tests are slow and environment-dependent.
  • Use pluginManagement in parent POMs to centralize plugin versions across modules.

Common Pitfalls

Dependency Hell and Version Conflicts

Symptom: NoSuchMethodError or ClassNotFoundException at runtime
This almost always means two versions of the same class are on the classpath. Maven resolved the wrong version via nearest-wins. Fix: run 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

MistakeSymptomFix
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

SNAPSHOT builds are not immutable
If your release depends on a SNAPSHOT, your build is non-reproducible — the same POM can produce different artifacts on different days. Always release against fixed versions. Use the enforcer plugin's requireReleaseDeps rule in your release profile.

Slow Builds

  • No parallelism: Add -T 1C for multi-module builds.
  • Re-downloading artifacts: Check network/proxy config; ensure ~/.m2 is cached in CI.
  • Integration tests in unit test phase: Name integration tests *IT.java and use Failsafe, not Surefire.
  • Slow test suite: Use -DskipTests during development iteration; run full suite only on CI.
  • Downloading SNAPSHOTs every time: Use -o for 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)

Shading merges duplicate resources — incorrectly by default
When using the Shade plugin to create a fat JAR, files like 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