Maven POM
last modified November 8, 2026
In this article we explain the Maven POM file. We cover project coordinates, packaging, properties, the effective POM, inheritance from a parent POM, and aggregation of modules. The examples use Maven 3.10 and Java 25.
The POM (Project Object Model) is an XML file called
pom.xml in the root directory of a project. It describes what the
project is, which libraries it depends on, and how it is built. Maven reads the
POM, merges it with its parents and built-in defaults, and runs the build from
the result.
POM structure
A POM can contain many elements. The following skeleton shows the most common
ones; only modelVersion and the coordinates are required.
<project>
<modelVersion>4.0.0</modelVersion>
<parent>...</parent> <!-- inheritance -->
<groupId>...</groupId> <!-- coordinates -->
<artifactId>...</artifactId>
<version>...</version>
<packaging>...</packaging>
<name>...</name> <!-- project information -->
<description>...</description>
<url>...</url>
<licenses>...</licenses>
<developers>...</developers>
<scm>...</scm>
<modules>...</modules> <!-- aggregation -->
<properties>...</properties>
<dependencyManagement>...</dependencyManagement>
<dependencies>...</dependencies>
<repositories>...</repositories>
<pluginRepositories>...</pluginRepositories>
<build> <!-- build configuration -->
<finalName>...</finalName>
<resources>...</resources>
<pluginManagement>...</pluginManagement>
<plugins>...</plugins>
</build>
<reporting>...</reporting>
<profiles>...</profiles>
<distributionManagement>...</distributionManagement>
</project>
The elements fall into a few groups: the identity of the project
(coordinates and project information), relations to other projects
(parent, modules, dependencies), the build
configuration, and environment-specific settings (repositories, profiles,
distribution). The order of the elements does not matter to Maven, but the
order shown here is the conventional one.
Minimal POM
The smallest valid POM contains the model version and the three coordinates.
<?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>
<groupId>com.example</groupId>
<artifactId>minimal</artifactId>
<version>1.0.0</version>
</project>
The modelVersion is 4.0.0 for Maven 2, 3, and 4. The XML
namespace and schema location are optional, but they let editors validate the
file and offer completion.
package com.example;
public class App {
public static void main(String[] args) {
System.out.println("A project built from a minimal POM");
}
}
The application is a regular Java class in the standard source directory
src/main/java.
Maven builds the project even though the POM says nothing about the build.
All the plugins and their versions come from defaults built into Maven. The
compiler warnings show the main weakness of a minimal POM: the compiler plugin
still defaults to Java 8 as the source and target level. Real projects set the
maven.compiler.release property.
$ mvn package [INFO] --- resources:3.5.0:resources (default-resources) @ minimal --- [INFO] --- compiler:3.16.0:compile (default-compile) @ minimal --- [WARNING] bootstrap class path is not set in conjunction with -source 8 [WARNING] source value 8 is obsolete and will be removed in a future release [WARNING] target value 8 is obsolete and will be removed in a future release [WARNING] To suppress warnings about obsolete options, use -Xlint:-options. [INFO] --- resources:3.5.0:testResources (default-testResources) @ minimal --- [INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ minimal --- [INFO] --- surefire:3.6.0:test (default-test) @ minimal --- [INFO] --- jar:3.5.1:jar (default-jar) @ minimal --- [INFO] Building jar: /home/user/minimal/target/minimal-1.0.0.jar [INFO] BUILD SUCCESS ... $ java -cp target/minimal-1.0.0.jar com.example.App A project built from a minimal POM
Coordinates and packaging
Every Maven artifact is identified by its coordinates, often written as
groupId:artifactId:version.
| Element | Meaning | Example |
|---|---|---|
groupId | the organization or project group, usually a reversed domain name | org.apache.commons |
artifactId | the name of the project; also the base name of the built file | commons-lang3 |
version | the version; a -SNAPSHOT suffix marks a development version | 3.21.0 |
packaging | the type of the artifact; jar by default | jar |
classifier | distinguishes extra files of the same version; used in dependencies, not in the project POM | sources |
The coordinates also determine where the artifact is stored in a repository:
the groupId with dots replaced by slashes, then the
artifactId and the version. For instance,
org.apache.commons:commons-lang3:3.21.0 is stored in
org/apache/commons/commons-lang3/3.21.0/.
The packaging decides which plugin goals are bound to the lifecycle phases.
The common values are jar for libraries and applications,
war for web applications, pom for parent and
aggregator projects, and maven-plugin for Maven plugins. A
pom project has no sources; its build only installs or deploys the
POM file.
The Super POM and the effective POM
Every POM implicitly inherits from the Super POM, a POM built into Maven. It defines the standard directory layout, the Maven Central repository, the default plugin versions, and, since Maven 3.10, the UTF-8 source encoding and the fixed timestamp for reproducible builds.
The effective POM is the result of merging a POM with its parents
and the Super POM, after properties are replaced and profiles are applied. It
is what Maven actually builds. The help:effective-pom goal prints
it. The full output is long; here we keep only some of the lines that came
from the Super POM.
None of these values are in our minimal POM. Each of them can be overridden
in the project POM; for instance, finalName in the
build section changes the name of the built JAR file. The
-Doutput=effective.xml option saves the effective POM to a file.
$ mvn help:effective-pom
<project.build.outputTimestamp>1980-02-01T00:00:00Z</project.build.outputTimestamp>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<url>https://repo.maven.apache.org/maven2</url>
<sourceDirectory>/home/user/minimal/src/main/java</sourceDirectory>
<testSourceDirectory>/home/user/minimal/src/test/java</testSourceDirectory>
<outputDirectory>/home/user/minimal/target/classes</outputDirectory>
<testOutputDirectory>/home/user/minimal/target/test-classes</testOutputDirectory>
<directory>/home/user/minimal/src/main/resources</directory>
<directory>/home/user/minimal/src/test/resources</directory>
<directory>/home/user/minimal/target</directory>
<finalName>minimal-1.0.0</finalName>
...
Properties
Properties are referenced with the ${name} syntax anywhere in
the POM. Maven replaces them when it builds the effective 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>props</artifactId>
<version>1.2.0</version>
<packaging>jar</packaging>
<name>Properties demo</name>
<description>Shows the kinds of properties available in a POM</description>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<greeting>Hello</greeting>
<app.title>${project.name} ${project.version}</app.title>
</properties>
</project>
The properties section defines our own properties and sets
properties read by plugins, such as maven.compiler.release.
<app.title>${project.name} ${project.version}</app.title>
A property can refer to other properties. The project.*
properties refer to elements of the POM itself, here the
name and version elements.
Maven knows the following kinds of properties:
project.*— elements of the effective POM, such asproject.versionorproject.build.directory- properties from the
propertiessection of the POM and its parents settings.*— values from thesettings.xmlfile, such assettings.localRepositoryenv.*— environment variables, such asenv.HOME- Java system properties, such as
java.versionoros.name, and properties given on the command line with-D
The help:evaluate goal prints the value of an expression. The
-q and -DforceStdout options make it print only the
value, which is useful in shell scripts.
$ mvn -q help:evaluate -Dexpression=app.title -DforceStdout Properties demo 1.2.0 $ mvn -q help:evaluate -Dexpression=project.build.finalName -DforceStdout props-1.2.0 $ mvn -q help:evaluate -Dexpression=project.build.directory -DforceStdout /home/user/props/target $ mvn -q help:evaluate -Dexpression=java.version -DforceStdout 25.0.3 $ mvn -q help:evaluate -Dexpression=env.HOME -DforceStdout /home/user $ mvn -q help:evaluate -Dexpression=settings.localRepository -DforceStdout /home/user/.m2/repository
Overriding properties
A property given with -D on the command line is a user property.
User properties take precedence over the properties defined in the POM, so we
can change a value without editing the file.
The greeting property is replaced by the command line value. The
project.version is not a property but an element of the POM, so
the -D option does not change it. Projects that want to set
their version from the command line use the ${revision}
placeholder in the version element.
$ mvn -q help:evaluate -Dexpression=greeting -DforceStdout Hello $ mvn -q help:evaluate -Dexpression=greeting -DforceStdout -Dgreeting=Hi Hi $ mvn -q help:evaluate -Dexpression=app.title -DforceStdout -Dproject.version=9.9 Properties demo 1.2.0
Inheritance and aggregation
Larger projects are split into several modules. Two separate mechanisms connect them:
- inheritance — a child POM names a parent in its
parentelement and inherits its configuration - aggregation — an aggregator POM lists modules in its
moduleselement; building the aggregator builds all the modules
The two are independent: a parent does not have to list its children, and an
aggregator does not have to be their parent. In practice, one POM in the root
directory usually plays both roles. Our shop project has a root POM and two
modules: core, a library, and app, an application
that uses it.
shop ├── app │ ├── pom.xml │ └── src/main/java/com/example/app/App.java ├── core │ ├── pom.xml │ └── src/main/java/com/example/core/Price.java └── pom.xml
The parent POM
The root POM is both the parent and the aggregator of the modules.
<?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>
<groupId>com.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>app</module>
<module>core</module>
</modules>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.21.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
A parent and an aggregator must have the pom packaging.
<modules>
<module>app</module>
<module>core</module>
</modules>
The modules element lists the directories of the modules. The
order does not matter: Maven sorts the modules so that each one is built after
the modules it depends on.
<dependencyManagement>
The dependencyManagement section sets the version of
commons-lang3 for all children. It does not add the library to
any module; a child still has to declare the dependency, but without a
version.
<pluginManagement>
Likewise, pluginManagement pins plugin versions and configuration
for the children without running the plugins in the parent itself.
The child POMs
The core module is a small library with a single record.
<?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>
<parent>
<groupId>com.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>core</artifactId>
</project>
The child names its parent by its coordinates. It inherits the
groupId and version, so it declares only its own
artifactId. Maven looks for the parent in the directory above,
../pom.xml, by default; a different location is set with the
relativePath element.
package com.example.core;
import java.math.BigDecimal;
import java.math.RoundingMode;
public record Price(BigDecimal amount) {
public Price withVat(int percent) {
var factor = BigDecimal.valueOf(100 + percent).movePointLeft(2);
return new Price(amount.multiply(factor).setScale(2, RoundingMode.HALF_UP));
}
@Override
public String toString() {
return amount + " EUR";
}
}
The Price record computes a price with VAT.
<?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>
<parent>
<groupId>com.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>app</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>
The app module depends on the core module and on Commons Lang. The
${project.version} reference keeps the module versions in sync;
the Commons Lang version comes from the parent's dependency management.
package com.example.app;
import com.example.core.Price;
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
public class App {
public static void main(String[] args) {
var price = new Price(new BigDecimal("49.90"));
System.out.println(StringUtils.rightPad("Net price:", 12) + price);
System.out.println(StringUtils.rightPad("With VAT:", 12) + price.withVat(23));
}
}
The application prints a price with and without VAT.
We run the build in the root directory. The reactor, the part of Maven that
handles multi-module builds, puts core before app,
although the modules element lists them in the opposite order.
Then we run the application with the two JAR files and Commons Lang on the
classpath.
$ mvn package [INFO] Reactor Build Order: [INFO] shop-parent [pom] [INFO] core [jar] [INFO] app [jar] [INFO] Building shop-parent 1.0.0 [1/3] [INFO] Building core 1.0.0 [2/3] [INFO] Building app 1.0.0 [3/3] [INFO] Reactor Summary for shop-parent 1.0.0: [INFO] shop-parent ........................................ SUCCESS [ 0.001 s] [INFO] core ............................................... SUCCESS [ 1.542 s] [INFO] app ................................................ SUCCESS [ 0.187 s] [INFO] BUILD SUCCESS ... $ java -cp app/target/app-1.0.0.jar:core/target/core-1.0.0.jar:\ $HOME/.m2/repository/org/apache/commons/commons-lang3/3.21.0/commons-lang3-3.21.0.jar \ com.example.app.App Net price: 49.90 EUR With VAT: 61.38 EUR
Inspecting inheritance
The -Dverbose option of help:effective-pom adds a
comment to each line telling which POM, and which line of it, the value comes
from. The -pl app option selects the app module. We show the
part of the output with the coordinates, properties, and dependencies.
The groupId, the version, and the Java release come
from the parent, the source encoding comes from the Super POM, and the version
of commons-lang3 is filled in from the parent's dependency
management. The output is a quick way to find out why a module uses a
particular value.
$ mvn help:effective-pom -Dverbose -pl app
<groupId>com.example</groupId> <!-- com.example:shop-parent:1.0.0, line 7 -->
<artifactId>app</artifactId> <!-- com.example:app:1.0.0, line 13 -->
<version>1.0.0</version> <!-- com.example:shop-parent:1.0.0, line 9 -->
<properties>
<maven.compiler.release>25</maven.compiler.release> <!-- com.example:shop-parent:1.0.0, line 18 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- org.apache.maven:maven-model-builder:3.10.0:super-pom, line 27 -->
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId> <!-- com.example:shop-parent:1.0.0, line 24 -->
<artifactId>commons-lang3</artifactId> <!-- com.example:shop-parent:1.0.0, line 25 -->
<version>3.21.0</version> <!-- com.example:shop-parent:1.0.0, line 26 -->
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId> <!-- com.example:app:1.0.0, line 17 -->
<artifactId>core</artifactId> <!-- com.example:app:1.0.0, line 18 -->
<version>1.0.0</version> <!-- com.example:app:1.0.0, line 19 -->
</dependency>
<dependency>
<groupId>org.apache.commons</groupId> <!-- com.example:app:1.0.0, line 22 -->
<artifactId>commons-lang3</artifactId> <!-- com.example:app:1.0.0, line 23 -->
<version>3.21.0</version> <!-- com.example:shop-parent:1.0.0, line 26 -->
</dependency>
</dependencies>
What is inherited
A child inherits most of the parent's elements: the groupId and
version, properties, dependencies, dependency and plugin
management, plugins and their configuration, repositories, and project
information such as description, url,
licenses, and developers. A few elements are not
inherited: the artifactId, name,
packaging, modules, and prerequisites.
Profiles are not inherited either, but the effects of the profiles active in
a parent are.
Dependencies declared directly in the parent's dependencies
section are added to every child. This is convenient for something like a
logging API, but it forces the dependency on all modules; for most libraries,
dependency management in the parent and an explicit dependency in the child is
the better choice. To share versions with projects that have a different
parent, use a BOM imported with the import scope.
Source
In this article we have explained the structure of the Maven POM, the effective POM, properties, inheritance, and aggregation.
Author
List all Java tutorials.