Java Maven pom.xml
The Maven Project Object Model — pom.xml structure, coordinates, properties, and inheritance.
Java Maven pom.xml
The pom.xml is the heart of any Maven project. POM stands for Project Object Model — a single XML file that declares what your project is (its identity), what it needs (its dependencies), and how to build it (plugins and configuration). Maven reads this file, downloads everything it references from a repository, and runs the build. Where an ad-hoc project scatters this knowledge across shell scripts and a lib/ folder of hand-copied JARs, Maven puts it all in one declarative, version-controlled document.
The minimal POM
Every POM is an XML document rooted in a <project> element with the Maven 4.0.0 schema. The smallest useful POM declares its model version and its coordinates — the four values that name the artifact:
<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.w3docs</groupId>
<artifactId>shop-api</artifactId>
<version>1.4.0</version>
<packaging>jar</packaging>
</project><modelVersion> is always 4.0.0 — it is the version of the POM format, not of your code. Leave it exactly as shown.
Coordinates: groupId, artifactId, version
A Maven artifact is identified by its GAV coordinates. Every dependency you ever add is named by the same three (sometimes four) values, so it is worth learning them precisely:
| Element | Meaning | Convention |
|---|---|---|
groupId | The organization or namespace | Reverse-DNS, e.g. com.google.code.gson |
artifactId | The project name within the group | Lowercase, hyphenated, e.g. shop-api |
version | The release of this artifact | Semantic, e.g. 1.4.0; -SNAPSHOT for in-progress builds |
packaging | The output type | jar (default), war, pom |
Together, groupId:artifactId:version is globally unique. It is what Maven uses to locate a JAR in a repository and to name the artifact your build produces. A version ending in -SNAPSHOT (e.g. 1.5.0-SNAPSHOT) is a mutable, in-development build that Maven may re-download; a plain version is treated as immutable.
Declaring dependencies
Dependencies are listed under <dependencies>, each one a <dependency> block giving the coordinates of a library you need. Maven resolves them — and their dependencies, transitively — from a repository (Maven Central by default):
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>The <scope> controls when a dependency is on the classpath. compile (the default) means everywhere; test means only while compiling and running tests, so JUnit never ships inside your JAR. Other scopes include provided (supplied by the runtime, e.g. a servlet API) and runtime (needed to run but not to compile, e.g. a JDBC driver).
Properties: don't repeat versions
The <properties> element defines reusable variables, referenced elsewhere with ${name} syntax. The idiom is to pin every library version once at the top so upgrades happen in a single place:
<properties>
<maven.compiler.release>21</maven.compiler.release>
<junit.version>5.10.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>maven.compiler.release is a well-known property that tells the compiler plugin which Java release to target — cleaner than configuring the plugin by hand. When Maven reads the POM it interpolates every ${...} placeholder, substituting the property's value before resolving anything.
Inheritance with a parent POM
POMs form a hierarchy. A <parent> element makes a project inherit coordinates, properties, and dependency management from another POM. Spring Boot's starter parent is the classic example — it fixes a consistent set of library versions so you can omit <version> on managed dependencies:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>
<dependencies>
<dependency>
<!-- version omitted: inherited from the parent's dependencyManagement -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>A multi-module build uses the same mechanism in reverse: one aggregator POM with <packaging>pom</packaging> lists <modules>, and each child names it as <parent>. Shared configuration lives in one place; the children stay tiny.
A worked example: reading a POM the way Maven does
A pom.xml is just XML, so we can parse one with the JDK's built-in DOM API and walk its structure exactly as Maven would — reading the coordinates, the properties, and each dependency, while interpolating a ${...} placeholder by hand. (Maven itself is a build tool, not a library on the classpath here, but this shows the model it operates on.)
What to take from the run:
- The
modelVersionprinted as4.0.0— the constant every POM carries. It identifies the POM format, not your project, and Maven would reject a POM that omitted it. - The coordinates came out as
com.w3docs:shop-api:1.4.0, the GAV triple in its canonicalgroupId:artifactId:versionform. This single string is how Maven names both the artifact this build produces and every dependency it resolves. packagingread back asjar, the default output type. Had it beenpom, this would be an aggregator/parent project that produces no JAR of its own.- The
junit.versionproperty resolved to5.10.2, and the JUnit dependency's${junit.version}placeholder was interpolated to that same value — exactly the substitution Maven performs so a version is declared in one place and reused. - The dependency walk reported two dependencies and printed each with its scope: Gson defaulted to
compile(no<scope>element, so it is on every classpath), while JUnit showedscope=test, keeping it out of the shipped artifact.
Practice
In a pom.xml, what is the purpose of the three elements groupId, artifactId, and version together?