Build Tools & Modules

Maven Basics

15 min Lesson 2 of 13

Maven Basics

Maven is the dominant build tool in the Java ecosystem and the de-facto standard for enterprise projects. Understanding it deeply — not just copying pom.xml snippets — separates developers who fight their build from those who control it. This lesson covers the three pillars that make Maven tick: the Project Object Model, project coordinates, and the build lifecycle.

What Maven Actually Does

Maven is a convention-over-configuration build tool. It defines a standard project layout and a fixed sequence of build steps. Follow the conventions and Maven handles compilation, testing, packaging, and deployment with almost no configuration. Deviate from them and you pay in complexity.

Maven also acts as a dependency manager: it downloads your libraries from central repositories, caches them locally, and resolves transitive dependencies automatically. That is the subject of Lesson 3; here we focus on the structural and lifecycle fundamentals.

The Standard Directory Layout

Every Maven project follows this layout out of the box:

my-app/ ├── pom.xml <-- the build descriptor └── src/ ├── main/ │ ├── java/ <-- production Java sources │ └── resources/ <-- production classpath resources └── test/ ├── java/ <-- test Java sources └── resources/ <-- test classpath resources
Respect the convention. The layout is not arbitrary — Maven plugins hard-code these paths. Moving source to src/ directly is possible but requires extra plugin configuration that every new team member must also understand. Save that cost: use the standard layout.

The Project Object Model (pom.xml)

The pom.xml is Maven's central configuration file. It is an XML document that describes what to build (not how — that is Maven's job). A minimal POM for a Java 21 library looks like this:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- Project coordinates --> <groupId>com.example</groupId> <artifactId>invoice-service</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>jar</packaging> <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> </project>

Every element earns its place. modelVersion is always 4.0.0 for Maven 2/3/4-compatible POMs. packaging defaults to jar but can be war, pom (for parent/aggregator projects), or custom types added by plugins.

Project Coordinates: groupId, artifactId, version

The three coordinate elements together form a globally unique identifier for your artifact — sometimes written as groupId:artifactId:version (GAV). Maven uses this triple to store artifacts in the local repository and to reference dependencies.

  • groupId — identifies your organisation or project family. Use a reversed domain name: com.example, org.apache.commons. This maps to a directory path in the repository.
  • artifactId — the name of this specific module. Lowercase, hyphen-separated: invoice-service, user-api. Together with the groupId it uniquely identifies a library.
  • version — follows Semantic Versioning (MAJOR.MINOR.PATCH). The -SNAPSHOT suffix has a special meaning: it signals a work-in-progress build. Maven treats SNAPSHOTs differently from releases (see note below).
SNAPSHOT vs. Release. A 1.0.0-SNAPSHOT artifact can be overwritten in a repository — Maven re-downloads it periodically because the content may change. A release version like 1.0.0 is immutable by convention: once published, it must never change. During active development use -SNAPSHOT; cut a release version when you are ready to ship. Never publish a -SNAPSHOT as a production dependency.

Properties and Variable Substitution

The <properties> section defines key-value pairs referenced anywhere in the POM with ${name} syntax. This is essential for single-point-of-truth version management:

<properties> <java.version>21</java.version> <jackson.version>2.17.1</jackson.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <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>

When you upgrade Jackson, you change one line. Without properties you risk the two artifacts drifting to different versions — a classic source of subtle runtime failures.

The Build Lifecycle

Maven defines three built-in lifecycles. The most important is the default lifecycle, which handles building and deploying your project. Its phases run in order and each phase triggers all preceding phases automatically:

  • validate — verify the project structure is correct.
  • compile — compile the production sources.
  • test — compile and run unit tests with src/test/java.
  • package — package the compiled code into its distributable format (JAR, WAR…).
  • verify — run integration tests and quality checks.
  • install — install the artifact into the local repository (~/.m2/repository), making it available to other local projects.
  • deploy — upload the artifact to a remote repository (Nexus, Artifactory, Maven Central).

The other two built-in lifecycles are clean (deletes the target/ directory) and site (generates project documentation). Running mvn clean package executes the clean lifecycle then the default lifecycle up to and including package.

# Common Maven commands and what they do mvn compile # compile main sources mvn test # compile + run unit tests mvn package # compile + test + create JAR/WAR in target/ mvn verify # package + integration tests mvn install # verify + install to ~/.m2 mvn deploy # install + upload to remote repository mvn clean package # delete target/, then package (recommended in CI) mvn clean install -DskipTests # fast local build skipping tests (use sparingly)
Avoid -DskipTests in CI. Skipping tests locally to iterate quickly is acceptable, but a CI pipeline that skips tests defeats the purpose of CI. If tests are slow, invest in making them faster rather than skipping them.

Plugins and Goal Binding

Maven's lifecycle phases are abstract — the actual work is performed by plugins bound to those phases. Each plugin exposes one or more goals. For example, the maven-compiler-plugin binds its compile goal to the compile phase and its testCompile goal to the test-compile phase. This happens automatically for JAR packaging.

You configure plugins in the <build> section. Pinning plugin versions is a professional practice — it ensures your build is reproducible months or years later:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <release>21</release> <compilerArgs> <arg>-Xlint:all</arg> </compilerArgs> </configuration> </plugin> </plugins> </build>

The <release> flag (preferred over the older <source>/<target> pair) sets the Java version for compilation, bytecode level, and the bootstrap classpath in one shot, preventing hard-to-diagnose cross-version issues.

The Local Repository Cache

When Maven downloads a dependency it stores it in ~/.m2/repository using the GAV path: com/example/invoice-service/1.0.0/invoice-service-1.0.0.jar. Subsequent builds reuse the cached artifact without a network call. This cache is shared across all Maven projects on your machine. On a CI server each build agent typically starts with an empty cache — use caching features of your CI system to preserve ~/.m2/repository between builds and dramatically cut build times.

Summary

Maven brings structure through conventions: a fixed directory layout, a declarative pom.xml that describes your project with coordinates (groupId:artifactId:version), and a deterministic default lifecycle (validate → compile → test → package → verify → install → deploy). Plugins bind goals to lifecycle phases and do the actual work. Pin plugin versions, always set <release> for the compiler, use properties for shared version strings, and keep SNAPSHOTs out of production dependencies. With these fundamentals in place you can read and reason about any Maven project you encounter in the wild.