Maven: POM Fundamentals

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Maven 3.10.0 (released on 27 September 2026). Every example marked as run was executed on JDK 26 on 2026-10-16, on macOS, in a scratch directory, with Maven 3.10.0 unless the text names another version; the 3.9.16 and 4.0.0-rc-7 (release candidate) runs are called out where their behaviour differs. Output is shown as printed, with the long scratch path shortened to <scratch> or <dir>, with ... on a line of its own where lines were left out, and with the lines made only of dashes and some empty [INFO] lines dropped without a marker. The examples use the running scenario of this section, tidepool (see the section landing page): a parent POM io.tidepool:tidepool-parent:1.0.0 that aggregates tidepool-model, tidepool-engine and tidepool-cli. Several examples need a parent and a child, or a repository, to show a behaviour; those are small throw-away projects and are described where they appear.

The POM is the one file every Maven project has, and almost every other page of this section is about one of its elements. This page is the map: what a POM is, the smallest one that works and what Maven adds around it, the groups of elements, and then the parts that are not covered elsewhere: coordinates and packaging as they appear in the file, properties and the variables Maven substitutes into the file (interpolation), the descriptive and environmental information about a project, and the model version. The big structural elements have their own pages: parents, aggregation and the effective POM, dependencies, dependencyManagement, plugins and the lifecycle, and profiles. The ideas behind the names are on Philosophy, Coordinates & Directory Layout.

What a POM is

POM stands for Project Object Model. It is an XML representation of a Maven project held in a file named pom.xml. The Maven site calls it a one-stop shop for everything about the project: the dependencies, the plugins and goals that can run, the build profiles, and also the version, the description, the developers, the mailing lists, the licenses, the issue tracker and the organization. In the Maven world a project does not even need to contain code, only a pom.xml; the parent POM of a multi-module build is exactly that.

The POM is declarative. The Maven documentation puts it as: the POM is the "who", "what" and "where" of a project, while the build lifecycle is the "when" and "how". A POM states its configuration; it does not list the steps. That difference from an Ant build.xml (a procedural list of targets and tasks, see Ant: Build File Structure) has a practical consequence the Maven site points out: if something outside the POM makes the lifecycle skip a plugin execution, the plugins that do run still do their work, whereas in an Ant file a task usually depends on the lines that ran before it. The POM can still affect the flow of the build (that is what binding a plugin goal to a phase does), but it does so by declaration.

The minimal POM

The minimum that Maven accepts is the project element with four children: modelVersion (set to 4.0.0), groupId, artifactId and version. Those three coordinates together form the project’s fully qualified artifact name, groupId:artifactId:version. Everything else has a default. This is the whole of the tidepool minimal project:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-minimal</artifactId>
  <version>1.0.0</version>
</project>

With no sources and no other file in the directory, mvn -B -ntp package on Maven 3.10.0 builds (lines about the JAR; the lines for resources, compiler and surefire say that there is nothing to do):

[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-minimal ---
[INFO] No sources to compile
...
[INFO] --- surefire:3.6.0:test (default-test) @ tidepool-minimal ---
[INFO] No tests to run.
[INFO]
[INFO] --- jar:3.5.1:jar (default-jar) @ tidepool-minimal ---
[WARNING] JAR will be empty - no content was marked for inclusion!
[INFO] Building jar: <scratch>/min/target/tidepool-minimal-1.0.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

Six lines of XML produced a build that ran the whole default lifecycle for the jar packaging, because the omitted packaging is jar, and the lifecycle mapping of jar supplies the plugins. That is convention over configuration in its most compact form, and it is also a useful test when a build behaves oddly: reduce the POM to this and add elements back until the behaviour reappears.

What Maven adds around the minimal POM

The effective POM is the POM as Maven actually uses it, after inheritance from the super POM, interpolation and the default lifecycle bindings have been applied. help:effective-pom prints it. For the six-line POM above:

mvn -B -ntp -q help:effective-pom -Doutput=effective.xml
wc -l effective.xml

printed 219 lines on Maven 3.10.0, 234 on Maven 3.9.16 and 172 on Maven 4.0.0-rc-7 (release candidate). The start of the 3.10.0 file shows the three kinds of thing that are added (a ... marks the lines that were left out, here the plugin executions):

<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>io.tidepool</groupId>
  <artifactId>tidepool-minimal</artifactId>
  <version>1.0.0</version>
  <properties>
    <project.build.outputTimestamp>1980-02-01T00:00:00Z</project.build.outputTimestamp>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  </properties>
  <repositories>
    <repository>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
      <id>central</id>
      <name>Central Repository</name>
      <url>https://repo.maven.apache.org/maven2</url>
    </repository>
  </repositories>
  ...
  <build>
    <sourceDirectory><dir>/src/main/java</sourceDirectory>
    <scriptSourceDirectory><dir>/src/main/scripts</scriptSourceDirectory>
    <testSourceDirectory><dir>/src/test/java</testSourceDirectory>
    <outputDirectory><dir>/target/classes</outputDirectory>
    <testOutputDirectory><dir>/target/test-classes</testOutputDirectory>
    <resources>
      <resource>
        <directory><dir>/src/main/resources</directory>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <directory><dir>/src/test/resources</directory>
      </testResource>
    </testResources>
    <directory><dir>/target</directory>
    <finalName>tidepool-minimal-1.0.0</finalName>
    <plugins>
      <plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>3.5.0</version>
        <executions>
          <execution>
            <id>default-clean</id>
            <phase>clean</phase>
            ...
  • The super POM contributes the Central repository (for dependencies and for plugins), the directory layout and the default finalName. The standard layout is not a convention written somewhere in the documentation; it is <build> elements with default values.

  • The packaging contributes the plugin versions and the executions, with the default- execution ids (see Build Lifecycle). The default jar is not written back to the effective POM, so a packaging element is absent from the file.

  • The three <properties> are new in Maven 3.10.0: its super POM sets the source and reporting encodings and a fixed project.build.outputTimestamp, which makes archives reproducible by default. The 3.10.0 release notes list these changes, together with the removal of the <pluginManagement> section and of the release-profile from the super POM. The 3.9.16 effective POM has no <properties> at all and does manage plugin versions. The consequence for encodings is visible in a build: with no project.build.sourceEncoding, Maven 3.9.16 printed File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! when compiling and Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! for resources, while 3.10.0 printed neither and help:evaluate reported UTF-8 for the expression. Maven 4.0.0-rc-7 (release candidate) also reported UTF-8.

The rest of the difference between the three Maven versions (the 3.9.16 super POM’s <pluginManagement>, for instance) is explained on Inheritance, Aggregation & the Effective POM.

When a required element is missing

A POM that leaves out a required element does not build. Each row below is the minimal POM with the one element removed; the message is from mvn -B -ntp validate on Maven 3.10.0, and the last column says what 4.0.0-rc-7 (release candidate) did with the same file.

Element removed Maven 3.10.0 message Maven 4.0.0-rc-7 (release candidate)

modelVersion

'modelVersion' is missing. @ line 1, column 52 (printed twice)

builds: BUILD SUCCESS, the element is not needed

artifactId

[FATAL] 'artifactId' is missing. @ line 1, column 52, project shown as io.tidepool:[unknown-artifact-id]:1.0.0

fails with 2 problems, the same message with line 1, column 1

groupId

[FATAL] 'groupId' is missing. @ line 1, column 52, project shown as [unknown-group-id]:tidepool-minimal:1.0.0

fails with 2 problems

version

[FATAL] 'version' is missing. @ line 1, column 52, project shown as io.tidepool:tidepool-minimal:[unknown-version]

fails with 2 problems

packaging set to jarr

Unknown packaging: jarr @ line 6, column 14

fails with 2 problems (the same message twice)

The 3.10.0 output for the missing artifactId, in full, shows the shape of every such failure (the first block of lines names the problem, the [Help 1] lines at the end are the generic pointer to the Maven wiki):

[INFO] Scanning for projects...
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[FATAL] 'artifactId' is missing. @ line 1, column 52
 @
[ERROR] The build could not read 1 project -> [Help 1]
[ERROR]
[ERROR]   The project io.tidepool:[unknown-artifact-id]:1.0.0 (<dir>/pom.xml) has 1 error
[ERROR]     'artifactId' is missing. @ line 1, column 52
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.

groupId and version may be omitted when a <parent> supplies them (they are inherited; artifactId and packaging never are), which is why a module POM is usually shorter than the minimal one. A misspelt element name is a different failure: a POM with <dependecies> instead of <dependencies> stops with Malformed POM <dir>/pom.xml: Unrecognised tag: 'dependecies' on 3.9.16 and 3.10.0, and with Unable to read model: Unrecognised tag: '{http://maven.apache.org/POM/4.0.0}dependecies' on 4.0.0-rc-7. Maven does not validate against the XSD, though: the same POM with no xmlns at all built on all three versions. The namespace and the xsi:schemaLocation matter for editors and tools that validate and complete, not for Maven itself.

The elements under project

The POM reference lists the elements that may appear directly under project, in five groups. Knowing the groups is enough to find where a topic lives:

Group Elements Covered on

The basics

groupId, artifactId, version, packaging

this page

dependencies, dependencyManagement

Dependencies, Scopes & Mediation, Dependency Management & BOMs

parent, modules

Inheritance, Aggregation & the Effective POM, Multi-Module Builds & the Reactor

properties

this page

Build settings

build (directories, resources, plugins, pluginManagement, extensions), reporting

Plugins & Their Configuration, Resources & Filtering, Site & Reporting

More project information

name, description, url, inceptionYear, licenses, organization, developers, contributors

this page

Environment settings

issueManagement, ciManagement, mailingLists, scm, prerequisites

this page

repositories, pluginRepositories

Repositories, Mirrors & Repository Managers

distributionManagement

this page (relocation, status), Install, Deploy & Distribution Management

Profiles

profiles

Profiles

The POM reference page says that modelVersion "is currently the only supported POM version" and is always required. That sentence predates Maven 4, which adds model version 4.1.0; the last section of this page runs both.

Coordinates and packaging in the file

The three coordinates act, in the words of the POM reference, like an address and a timestamp in one: they mark a specific place in a repository. groupId is generally unique among an organization or a project, and does not have to match the Java package structure (though following it is good practice); in a repository the dots become directory separators. artifactId is the name the project is known by and, with the groupId, the key that separates it from every other project. version separates incarnations of the same project, and is also a directory in the repository. A project with groupId org.codehaus.mojo, artifactId my-project and version 1.0 lives in $M2_REPO/org/codehaus/mojo/my-project/1.0. The reasoning behind the scheme, classifiers and the type-versus-extension difference are on the Philosophy page; the format of versions and ranges is on Versions, SNAPSHOTs & Ranges.

packaging

packaging is the fourth coordinate of the project’s identity. When it is absent it is jar. Its value is not free text: it is the role-hint of a lifecycle mapping, which fixes the list of plugin goals that run in each phase of the default lifecycle. The POM reference lists these core packaging values: pom, jar, maven-plugin, ejb, war, ear, rar. Plugins and extensions can contribute more; the Maven Central requirements list par, aar and apklib as further examples seen in the wild. The default bindings per packaging are on Build Lifecycle, and the archives themselves on Packaging JARs, WARs & Manifests.

Two runs show what the value changes. A pom project has no code to build and the lifecycle mapping contains no plugin for the early phases, so mvn -B -ntp package on the tidepool-parent POM runs nothing:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-parent</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>
</project>
[INFO] Building tidepool-parent 1.0.0
[INFO]   from pom.xml
[INFO] --------------------------------[ pom ]---------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

and install ran a single plugin goal, which copies the POM itself into the repository (this run used a throw-away -Dmaven.repo.local, so nothing was added to the real local repository):

[INFO] --- install:3.2.0:install (default-install) @ tidepool-parent ---
[INFO] Installing <dir>/pom.xml to <dir>/repo/io/tidepool/tidepool-parent/1.0.0/tidepool-parent-1.0.0.pom
[INFO] BUILD SUCCESS

A project that declares war gets the war plugin bound to package, and with no web descriptor that goal fails, which is the classic first error of a new web project (the war plugin 3.5.1 on Maven 3.10.0):

[INFO] --- war:3.5.1:war (default-war) @ tidepool-web ---
[INFO] Packaging webapp
[INFO] Assembling webapp [tidepool-web] in [<dir>/target/tidepool-web-1.0.0]
[INFO] Processing war project
[INFO] Building war: <dir>/target/tidepool-web-1.0.0.war
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-war-plugin:3.5.1:war (default-war) on project tidepool-web: Error assembling WAR: webxml attribute is required (or pre-existing WEB-INF/web.xml if executing in update mode) -> [Help 1]

The two fixes are to add a src/main/webapp/WEB-INF/web.xml or to set failOnMissingWebXml to false in the plugin configuration (this page does not run the second one; see Plugins & Their Configuration). And a value that is not a known packaging stops before anything runs, as in the table above (Unknown packaging: jarr).

finalName

<build><finalName> is the name of the file Maven creates in target, without its extension. Its default is ${project.artifactId}-${project.version} (the effective POM above shows tidepool-minimal-1.0.0). It is the one coordinate-related setting that people change on a project that has to produce a fixed file name, for example a web application called ROOT.war or a CLI called tidepool.jar, and it is also the source of a classic misunderstanding: it renames the file in target, not the artifact in a repository. A tidepool-model project with <finalName>tidepool</finalName>:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-model</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <build>
    <finalName>tidepool</finalName>
  </build>
</project>

built with mvn -B -ntp -q -Dmaven.repo.local=<scratch>/repo install (the -q hides the build log), produced

target/tidepool.jar
repo/io/tidepool/tidepool-model/1.0.0/_remote.repositories
repo/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
repo/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
repo/io/tidepool/tidepool-model/maven-metadata-local.xml

The file in target is called tidepool.jar, and the file installed in the repository is tidepool-model-1.0.0.jar. A repository has a fixed layout (artifactId-version[-classifier].extension below the coordinates' directory) and a consumer finds a file by coordinates, so the install and deploy plugins ignore finalName. Anyone who needs the plain name, such as a Dockerfile that copies target/tidepool.jar, uses finalName; anyone who publishes does not need to care about it.

Properties

Maven properties are value placeholders, like properties in Ant. They are declared in the <properties> element, and their value is available anywhere in a POM through the notation ${name}; plugins also use them as defaults for their parameters. The POM reference’s own example declares maven.compiler.source and maven.compiler.target as 1.7. That example no longer works on a current JDK: with those two properties set to 1.7 on JDK 26, compiler plugin 3.16.0 (Maven 3.10.0) printed

[WARNING] bootstrap class path is not set in conjunction with -source 7
[ERROR] COMPILATION ERROR :
[ERROR] Source option 7 is no longer supported. Use 8 or later.
[ERROR] Target option 7 is no longer supported. Use 8 or later.

and the build failed. Use maven.compiler.release (as the examples in this section do) or a toolchain; Compiling & Toolchains explains the difference.

The five styles of property

The POM reference names five kinds of property. The table has the reference’s description and, in the last column, what the expression gave in a run: a throw-away POM (the coordinates of tidepool-model) declared the <properties> shown below, and help:effective-pom printed the interpolated values. The Java system property was java.specification.version and os.name, and the environment variable was HOME.

<properties>
  <station>Tarifa</station>
  <by.project>${project.groupId}:${project.artifactId}:${project.version}</by.project>
  <by.property>station is ${station}</by.property>
  <by.system>${java.specification.version} on ${os.name}</by.system>
  <by.env>${env.HOME}</by.env>
  <by.settings>${settings.localRepository}</by.settings>
</properties>
Style Meaning Result in the run

env.X

the shell environment variable X; names are normalised to upper case, and the lookup is case-sensitive

${env.HOME} gave the home directory on all three Maven versions

project.x

the dot-notated path of an element in the POM

${project.groupId}:${project.artifactId}:${project.version} gave io.tidepool:tidepool-model:1.0.0

settings.x

the dot-notated path of an element in settings.xml

${settings.localRepository} was not replaced in the POM: help:effective-pom still showed ${settings.localRepository} (see below)

Java system properties

anything in java.lang.System.getProperties()

${java.specification.version} on ${os.name} gave 26 on Mac OS X

x

a name declared in <properties>

station is ${station} gave station is Tarifa

The reference adds a detail about Windows that this page cannot run (not run here, macOS): environment variable names are case-insensitive there, but the lookup of the property is case-sensitive, so ${env.Path} and ${env.PATH} are different expressions, and names are normalised to upper case.

The settings.* row is the odd one. The model builder reference explains why: after interpolation, ${...} content can remain in the model and be evaluated later when plugin parameters are set, and it names the settings.* values for the settings model as the main case. So settings.* is not a POM-interpolation prefix in practice; it is available to plugin parameters. help:evaluate uses the same expression evaluator as plugin parameters, and it does resolve it:

mvn -B -ntp -q help:evaluate -Dexpression=settings.localRepository -DforceStdout
<home>/.m2/repository

(The <home> stands for the home directory.) Reading settings.xml values is therefore possible in a plugin’s <configuration>, but not in a property of the POM or in a dependency version.

Properties Maven and its plugins know

Some property names are read by Maven or by the default plugins, so declaring them changes behaviour:

  • project.build.sourceEncoding and project.reporting.outputEncoding set the encoding of source files and reports. The model builder reference explains that they are POM properties "looking like POM content" that were not added to the model, to remain compatible with older versions; their default was the platform encoding (and UTF-8 for reports) until Maven 3.10.0 set UTF-8 in the super POM.

  • project.build.outputTimestamp makes archives reproducible; Maven 3.10.0 sets it to 1980-02-01T00:00:00Z in the super POM (see above). It is the subject of Security & Reproducible Builds.

  • maven.compiler.release, maven.compiler.source and maven.compiler.target are the user properties of the compiler plugin.

  • maven.build.timestamp.format customises the build timestamp (below).

  • project.directory, available since Maven 3.5.0, changes the path that is appended when a child inherits URLs (see URLs and SCM paths in a child).

The POM reference warns in a comment of its own example not to start your own properties with project.: those names are reserved for Maven and "will become elements in a future POM definition". Section "Precedence" below shows what happens when you do.

Precedence

A property can be declared in several places at once, and the values are not equal. A POM with the following properties (the coordinates are those of tidepool-model) was run with and without -D options on the command line:

<properties>
  <station>Tarifa</station>
  <java.specification.version>from-the-pom</java.specification.version>
  <env.USER>from-the-pom</env.USER>
  <project.artifactId>from-the-pom</project.artifactId>
  <result.station>${station}</result.station>
  <result.java>${java.specification.version}</result.java>
  <result.env>${env.USER}</result.env>
  <result.artifact>${project.artifactId}</result.artifact>
</properties>

The table lists the value each result.* property had in the effective POM:

Case Maven 3.9.16 Maven 3.10.0 Maven 4.0.0-rc-7 (release candidate) Winner

${station}, no option

Tarifa

Tarifa

Tarifa

the POM

${station} with -Dstation=Cadiz

Cadiz

Cadiz

Cadiz

the command line

${java.specification.version}

from-the-pom

from-the-pom

from-the-pom

the POM beats the JVM system property

${java.specification.version} with -D...=from-cli

from-cli

from-cli

from-cli

the command line

${env.USER}

from-the-pom

from-the-pom

from-the-pom

the POM beats the environment

${project.artifactId}, no option

tidepool-model

tidepool-model

from-the-pom

the model in 3.x, the property in 4

${project.artifactId} with -Dproject.artifactId=cli

tidepool-model

tidepool-model

cli

the model in 3.x, the command line in 4

So, on all three versions, user properties from -D win over properties in the POM, and properties in the POM win over system properties and environment variables, which is what lets a POM supply a default that the command line overrides. The row that differs is the one the POM reference tells you to avoid: a user-declared project.artifactId is ignored by Maven 3.x (the model value is used first, matching the model builder reference, which lists project.* first) and honoured by Maven 4.0.0-rc-7. Declaring properties under the project. prefix therefore behaves differently between Maven 3 and Maven 4 and should not be done.

Overriding from the command line

Because -D wins, a POM property is a build parameter. Declare <greeting>from-the-pom</greeting>, use ${greeting} in a place that accepts properties, and call mvn -Dgreeting=from-the-command-line .... Beware that the same mechanism makes -D a global switch: a system property given with -D is visible to every module and every plugin of the build. The CLI options themselves are on Running Maven (CLI), and profiles are the structured way to switch whole groups of properties.

Project model variables

Any single-valued field of the POM model can be referenced with the prefix project.: ${project.groupId}, ${project.version}, ${project.build.sourceDirectory} and so on. The POM reference lists the fields. help:evaluate prints the value Maven has for any of them, including values the POM never wrote. For a child of the parent used in the "Project information" section below (it declares a <parent> and an artifactId, and nothing else):

mvn -B -ntp -q help:evaluate -Dexpression=project.groupId -DforceStdout

gave these values on Maven 3.10.0 (one expression per run; the output has no newline, the table puts the results together):

Expression Value

project.groupId

io.tidepool (inherited from the parent)

project.version

1.0.0 (inherited)

project.parent.artifactId

tidepool-parent

project.packaging

jar (the default; never inherited)

project.name

tidepool-model (a project with no name reports its artifactId; the name is not inherited)

project.description

Tide predictions from harmonic constituents. (inherited)

project.url

https://tidepool.example.org/tidepool-model/ (inherited, with the artifactId appended)

project.build.finalName

tidepool-model-1.0.0

project.build.directory

<dir>/target

project.organization.name

Tidepool Project (inherited)

project.licenses[0].name

Apache-2.0 (a list element is addressed by index)

An expression that names a list or a complex object (project.developers, project.modules, project.properties) prints it as XML. On JDK 26 that also printed WARNING: A terminally deprecated method in sun.misc.Unsafe has been called on standard error, a message from the XML serialisation library the help plugin uses and not from the build; it goes away by asking for a scalar field.

The prefixes pom. and the empty prefix (${artifactId}, ${version}) work as aliases but are deprecated, and the introduction to the POM says they "should not be used". The runs agree on the deprecation and differ on what happens next. The throw-away POM above also had <by.pom-prefix>${pom.artifactId}</by.pom-prefix> and <by.bare>${artifactId}</by.bare>; mvn -B -ntp validate printed on Maven 3.9.16 and 3.10.0:

[WARNING] Some problems were encountered while building the effective model for io.tidepool:tidepool-model:jar:1.0.0
[WARNING] The expression ${pom.artifactId} is deprecated. Please use ${project.artifactId} instead.
[WARNING] The expression ${artifactId} is deprecated. Please use ${project.artifactId} instead.
[WARNING]
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.

and both expressions were replaced by tidepool-model. Maven 4.0.0-rc-7 (release candidate) printed no warning, still replaced ${artifactId} and left ${pom.artifactId} untouched in the effective POM. Use project. everywhere: it is the only prefix that behaves the same in all three versions.

Special variables

A few variables are not fields of the model. The introduction to the POM lists three, and the 3.10.0 release notes add the root directory:

Variable Meaning Since

${project.basedir}

the directory that the current project resides in

not stated

${project.baseUri}

the same directory, as a URI

Maven 2.1.0

${maven.build.timestamp}

the timestamp of the start of the build, in UTC

Maven 2.1.0-M1

${project.rootDirectory}

the project’s root directory, the one that contains the .mvn directory

a regular property in Maven 3.10.0 (and in Maven 4)

${maven.multiModuleProjectDirectory}

the root directory of a multi-module build, set by the mvn launcher script

set by the launcher

A two-project build (a parent in <scratch> with a module tidepool-model, and an empty .mvn directory next to the parent POM to mark the root) declared in the parent’s <properties>

<maven.build.timestamp.format>yyyy-MM-dd'T'HH:mm'Z'</maven.build.timestamp.format>
<v.basedir>${project.basedir}</v.basedir>
<v.baseUri>${project.baseUri}</v.baseUri>
<v.timestamp>${maven.build.timestamp}</v.timestamp>
<v.multiModule>${maven.multiModuleProjectDirectory}</v.multiModule>
<v.rootDirectory>${project.rootDirectory}</v.rootDirectory>

and mvn -B -ntp -q -pl tidepool-model help:effective-pom printed these values for the module (run on all three versions; the timestamp was produced at 14:38 local time, UTC+2):

<v.baseUri>file://<dir>/tidepool-model/</v.baseUri>
<v.basedir><dir>/tidepool-model</v.basedir>
<v.multiModule><dir></v.multiModule>
<v.rootDirectory><dir></v.rootDirectory>
<v.timestamp>2026-10-16T12:38Z</v.timestamp>

On Maven 3.10.0 and 4.0.0-rc-7 (release candidate) all five were resolved exactly like this. Maven 3.9.16 printed the same lines except <v.rootDirectory>${project.rootDirectory}</v.rootDirectory>, left unresolved: that variable arrived in 3.10.0, where, according to the release notes, session.topDirectory, session.rootDirectory and project.rootDirectory were "promoted to regular Maven properties, usable in interpolation and in profile activation". ${project.basedir} is the module’s own directory, and ${project.rootDirectory} the root of the whole build, so a path to a file shared by all modules is ${project.rootDirectory}/config/checkstyle.xml and not a ../.. chain.

Three details about the timestamp. The format is that of SimpleDateFormat and defaults to yyyy-MM-dd'T'HH:mm:ss'Z'; the example changed it by declaring maven.build.timestamp.format, and the output has no seconds. The timestamp is in UTC, hence the two hours of difference above. And the timestamp changes with every build, so putting it in an artifact (a manifest entry, a filtered resource) makes the artifact different every time; the reproducible alternative is a fixed project.build.outputTimestamp (see Security & Reproducible Builds).

Finally, these variables are POM interpolation features, not plugin expressions: help:evaluate -Dexpression=project.baseUri and -Dexpression=maven.build.timestamp both printed null object or invalid expression, because the plugin evaluator does not know them. To see their values use help:effective-pom, as above.

Interpolation

Interpolation is the replacement of ${...} by a computed value. The model builder performs it as one step of building the effective model, and its place in the sequence explains most surprises:

graph LR RAW["Raw POMs: child, parents, super POM"] --> ACT["Profile activation"] ACT --> INH["Inheritance assembly"] INH --> INT["Interpolation of placeholders"] INT --> URL["URL normalisation"] URL --> PHASE2["Path translation, plugin management, lifecycle bindings, dependency management"] PHASE2 --> VAL["Effective model validation"]

Profile activation comes before interpolation, so a file-based profile activation condition can use only ${basedir}, system properties and user properties. Inheritance comes before interpolation too, which is the next section’s point.

Late binding: the child’s value wins

Because variables are processed after inheritance, a parent that uses a variable gets the definition of the child, not its own. The introduction to the POM says so, and a small experiment shows it. The parent declares and uses a property:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-parent</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>

  <properties>
    <station>Tarifa</station>
    <banner>Tides for ${station} (built from ${project.artifactId})</banner>
  </properties>
</project>

and the child, which lives in the child directory below it, redefines station:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>io.tidepool</groupId>
    <artifactId>tidepool-parent</artifactId>
    <version>1.0.0</version>
  </parent>
  <artifactId>tidepool-child</artifactId>

  <properties>
    <station>Cadiz</station>
  </properties>
</project>

mvn -B -ntp -q help:evaluate -Dexpression=banner -DforceStdout printed, on all three versions, in the child directory

Tides for Cadiz (built from tidepool-child)

and in the parent directory Tides for Tarifa (built from tidepool-parent). Neither station nor project.artifactId was read in the parent: the string was inherited unexpanded and expanded in the context of the project being built. This is the mechanism behind template-style parents ("define the version once, let children set the name") and behind bugs where a child’s property silently changes what a shared parent does.

Unresolved placeholders and typos

A ${...} that matches nothing is not an error by itself. The effective POM of the properties experiment kept <by.missing>${no.such.property}</by.missing> as a literal string on all three versions. The error appears where the value is needed. The most common case is a misspelt property in a dependency version:

<properties>
  <commons-csv.vesion>1.14.1</commons-csv.vesion>
</properties>

<dependencies>
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-csv</artifactId>
    <version>${commons-csv.version}</version>
  </dependency>
</dependencies>

(vesion in the property name.) The build stops before resolving anything, on Maven 3.9.16 and 3.10.0 with:

[INFO] Scanning for projects...
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[ERROR] 'dependencies.dependency.version' for org.apache.commons:commons-csv:jar must be a valid version but is '${commons-csv.version}'. @ line 15, column 16

Maven 4.0.0-rc-7 (release candidate) reports the same problem twice with the coordinates spelt out ('dependencies.dependency.version' for groupId='org.apache.commons', artifactId='commons-csv', type='jar' must be a valid version but is '${commons-csv.version}'). Read the placeholder in the message literally: it is the name the POM asked for, so compare it with the declaration.

No escape for a literal dollar

There is no escape syntax for a literal ${name} in a POM that Maven would not also replace. The experiment declared three properties after station=Tarifa: <esc.dollar>$${station}</esc.dollar>, <esc.backslash>\${station}</esc.backslash> and <esc.plain>${station}</esc.plain>. The effective POM had $Tarifa, \Tarifa and Tarifa on all three versions: the placeholder was replaced anyway and a stray character was left. Where a literal is needed in a filtered resource, the resources plugin has its own escape parameter (escapeString, see Resources & Filtering),.

Interpolation in POMs read from a repository

Interpolation of a project’s own POM can use anything: command-line properties, environment variables, system properties. A POM that Maven reads from a repository, because it is a dependency, a parent or an imported BOM, is a different matter, and Maven 3.10.0 changed the rule. The release notes list, under "Security Hardening", that such a model "contributes less to the build: restricted interpolation, profile activation limited to platform-derived criteria, recessive repository merging and rejection of system scoped dependencies", with the property maven.model.dependencyInterpolation.full as an opt-out.

The effect is easy to reproduce. A throw-away tidepool-probe POM (packaging pom) was installed in a scratch local repository with a dependency whose version is a placeholder that only a command-line property can fill:

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-csv</artifactId>
  <version>${csv.v}</version>
</dependency>

A consumer project depends on it with <type>pom</type>, and dependency:tree (plugin 3.11.0) was run with -Dcsv.v=1.14.1. The resolved tree on Maven 3.9.16:

[INFO] io.tidepool:tidepool-consumer:jar:1.0.0
[INFO] \- io.tidepool:tidepool-probe:pom:1.0.0:compile
[INFO]    \- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO]       +- commons-io:commons-io:jar:2.20.0:compile
[INFO]       \- commons-codec:commons-codec:jar:1.19.0:compile

and on Maven 3.10.0 with the same command:

[INFO] io.tidepool:tidepool-consumer:jar:1.0.0
[INFO] \- io.tidepool:tidepool-probe:pom:1.0.0:compile
[INFO]    \- org.apache.commons:commons-csv:jar:${csv.v}:compile

On Maven 4.0.0-rc-7 (release candidate) the commons-csv node was missing altogether. Adding -Dmaven.model.dependencyInterpolation.full=true to the 3.10.0 command restored the 3.9.16 tree. Without any -D, both 3.9.16 and 3.10.0 printed the literal ${csv.v} node. The lesson is for anyone who publishes: a POM that goes to a repository must be self-contained. A published POM that needs a -D property, an environment variable or a profile activated by the consumer’s machine is unreliable, and from Maven 3.10.0 on it is also ignored. Properties defined in the POM itself or in its parents still work. The dependency side is on Dependencies, Scopes & Mediation, and the consumer POM that Maven 4 generates so that the published file is flat is on Maven 4: POM Model & Consumer POM.

Project information

Several elements do not affect the build; they document the project, and plugins (the site, the Javadoc and the JAR plugins, and the publishing checks of repositories) read them. This section uses one parent POM with all of them set, written for the example project (the host names are the reserved example.org, and nothing in it is real):

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-parent</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>

  <name>Tidepool</name>
  <description>Tide predictions from harmonic constituents.</description>
  <url>https://tidepool.example.org/</url>
  <inceptionYear>2026</inceptionYear>

  <organization>
    <name>Tidepool Project</name>
    <url>https://tidepool.example.org/</url>
  </organization>

  <licenses>
    <license>
      <name>Apache-2.0</name>
      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
      <distribution>repo</distribution>
      <comments>A business-friendly OSS license</comments>
    </license>
  </licenses>

  <developers>
    <developer>
      <id>maintainer</id>
      <name>Tidepool Maintainer</name>
      <email>maintainer@example.org</email>
      <organization>Tidepool Project</organization>
      <organizationUrl>https://tidepool.example.org/</organizationUrl>
      <roles>
        <role>architect</role>
        <role>developer</role>
      </roles>
      <timezone>Europe/Madrid</timezone>
      <properties>
        <chat>#tidepool</chat>
      </properties>
    </developer>
  </developers>

  <scm>
    <connection>scm:git:https://git.example.org/tidepool/tidepool.git</connection>
    <developerConnection>scm:git:ssh://git@git.example.org/tidepool/tidepool.git</developerConnection>
    <tag>HEAD</tag>
    <url>https://git.example.org/tidepool/tidepool</url>
  </scm>

  <issueManagement>
    <system>GitHub Issues</system>
    <url>https://git.example.org/tidepool/tidepool/issues</url>
  </issueManagement>

  <ciManagement>
    <system>GitHub Actions</system>
    <url>https://git.example.org/tidepool/tidepool/actions</url>
  </ciManagement>
</project>

The elements, in the order of the reference:

  • name, description, url, inceptionYear: the display name (it is not the artifactId), a short human-readable description, the project home page and the year the project started. Plugins use them for the site and the Javadoc header.

  • licenses: the licenses that apply to this project, not to its dependencies. Each has name, url, comments and distribution, which is repo (the artifact may be downloaded from a repository) or manual (it must be installed by hand). Use an SPDX identifier in name when you can; tools that audit licenses then do not have to guess.

  • organization: the name and url of the organization that runs the project.

  • developers: the people immediately responsible for the code, not everyone who ever contributed, with id, name, email, url, organization, organizationUrl, roles, timezone (a time zone id such as Europe/Madrid is preferred over an offset because it does not change with daylight saving time) and free-form properties. contributors has the same fields without id and is the place for occasional contributors.

  • mailingLists: name, subscribe, unsubscribe, post, archive and otherArchives for each list.

  • scm: how to reach the source. connection is read-only access, developerConnection is write access, tag names the tag the project lives under (HEAD by default) and url is a browsable web location. The connection values follow Maven SCM’s format scm:<provider>:<provider_specific>; scm:git: is the one in use today, and the book examples with scm:svn: or scm:cvs: show how old the format is.

  • issueManagement: system and url of the defect tracker.

  • ciManagement: system, url and notifiers of the continuous integration service. The reference’s example uses Continuum, which has been retired (see the book notes at the end).

What a child inherits

A child of that parent (artifactId tidepool-model, nothing else but the <parent>) saw, in its effective POM, on all three Maven versions:

  • description, organization, licenses, developers, issueManagement and ciManagement inherited unchanged. The project name was not inherited (the child’s project.name is its artifactId).

  • the five URLs got the child’s artifactId appended:

<url>https://tidepool.example.org/tidepool-model/</url>
<connection>scm:git:https://git.example.org/tidepool/tidepool.git/tidepool-model</connection>
<developerConnection>scm:git:ssh://git@git.example.org/tidepool/tidepool.git/tidepool-model</developerConnection>
<url>https://git.example.org/tidepool/tidepool/tidepool-model</url>

The last is scm/url; the fifth, the distributionManagement/site/url, became file:///tmp/tidepool-site/tidepool-model/ in a variant of the parent with a <site> element. The model builder reference documents this special handling of project.url, project.scm.connection, project.scm.developerConnection, project.scm.url and project.distributionManagement.site.url: an unset value is the parent’s with the artifact id appended. That is right when each module has its own page below the parent’s, and wrong for a repository URL, where the SCM URL of a module in a git repository is the repository’s. Two settings change it:

  • an attribute on the parent’s element, available since Maven 3.6.1, turns the appending off. In the parent, <project child.project.url.inherit.append.path="false"> on the project element, <scm child.scm.connection.inherit.append.path="false" child.scm.developerConnection.inherit.append.path="false" child.scm.url.inherit.append.path="false"> on scm and <site child.site.url.inherit.append.path="false"> on site. With them, the child’s effective POM had the parent’s values unchanged (<url>https://tidepool.example.org/</url>, <connection>scm:git:https://git.example.org/tidepool/tidepool.git</connection>, <url>https://git.example.org/tidepool/tidepool</url> for the SCM), on 3.9.16, 3.10.0 and 4.0.0-rc-7. The child.project.url.inherit.append.path attribute belongs on the project element, not on url; put on url, it had no effect in a run.

  • a project.directory property in the child, available since Maven 3.5.0, replaces the artifactId in the appended path, for a module whose directory name is not its artifactId. A child with <project.directory>model</project.directory> had https://tidepool.example.org/model/ and scm:git:https://git.example.org/tidepool/tidepool.git/model. The property is not itself inherited.

What the artifacts carry

The descriptive elements are in the POM, and the POM travels with the artifact. The JAR built by the finalName example above contains a copy of the POM and a small properties file with the coordinates, which tools use to identify a JAR found outside a repository:

META-INF/maven/io.tidepool/tidepool-model/pom.xml
META-INF/maven/io.tidepool/tidepool-model/pom.properties
artifactId=tidepool-model
groupId=io.tidepool
version=1.0.0

The installed tidepool-model-1.0.0.pom was byte-for-byte identical to the project’s pom.xml (checked with cmp): in the Maven 3 line the file that is installed and deployed is the source POM, with its <parent> reference, its unexpanded ${...} placeholders and its profiles, which is why a consumer needs the parent in a repository too and why the previous section’s rule (a self-contained POM) matters. The consumer POM of Maven 4 is the change that makes the published file a flattened one.

Repositories that host public releases check the descriptive elements. Maven Central requires name, description and url, the license information, the developer information and the SCM information in the POM, as well as correct coordinates (a release version cannot end in -SNAPSHOT). The checklist and how to produce the Javadoc, sources and signature files are on Publishing to Maven Central.

Environment settings

prerequisites and the Enforcer

<prerequisites><maven> states a minimum Maven version. In POM 4.0.0 maven is the only element it has. Its role has narrowed over the years: the POM reference says that for a build-time prerequisite one should use the Maven Enforcer Plugin’s requireMavenVersion rule, and that the element is still used at run time for packaging maven-plugin, to make sure the minimum Maven version of a plugin is met (only in the plugin’s own pom.xml). A jar project that sets it is told off by all three Maven versions. This POM:

<prerequisites>
  <maven>3.99.0</maven>
</prerequisites>

printed the same warning on 3.9.16, 3.10.0 and 4.0.0-rc-7, and built successfully (the prerequisite was not enforced):

[WARNING] The project io.tidepool:tidepool-model:jar:1.0.0 uses prerequisites which is only intended for maven-plugin projects but not for non maven-plugin projects. For such purposes you should use the maven-enforcer-plugin. See https://maven.apache.org/enforcer/enforcer-rules/requireMavenVersion.html

The way to state a build requirement is the Enforcer plugin (3.6.3). Bound to the validate phase through an execution, with the rules in the execution’s configuration:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-enforcer-plugin</artifactId>
      <version>3.6.3</version>
      <executions>
        <execution>
          <id>enforce-versions</id>
          <goals>
            <goal>enforce</goal>
          </goals>
          <configuration>
            <rules>
              <requireMavenVersion>
                <version>[3.9.0,)</version>
                <message>Tidepool needs Maven 3.9.0 or newer.</message>
              </requireMavenVersion>
              <requireJavaVersion>
                <version>[17,)</version>
              </requireJavaVersion>
            </rules>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

mvn -B -ntp validate passed with 3.9.16, 3.10.0 and 4.0.0-rc-7 (release candidate):

[INFO] --- enforcer:3.6.3:enforce (enforce-versions) @ tidepool-model ---
[INFO] Rule 0: org.apache.maven.enforcer.rules.version.RequireMavenVersion passed
[INFO] Rule 1: org.apache.maven.enforcer.rules.version.RequireJavaVersion passed
[INFO] BUILD SUCCESS

and with the range changed to [3.99.0,) it failed on 3.10.0 with the message that the POM supplies:

[INFO] --- enforcer:3.6.3:enforce (enforce-versions) @ tidepool-model ---
[INFO] Rule 1: org.apache.maven.enforcer.rules.version.RequireJavaVersion passed
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce (enforce-versions) on project tidepool-model:
[ERROR] Rule 0: org.apache.maven.enforcer.rules.version.RequireMavenVersion failed with message:
[ERROR] Tidepool needs Maven 3.99.0 or newer.

The prerequisites run for a maven-plugin project was not run here; the other rules of the Enforcer are on Enforcer.

repositories and pluginRepositories

<repositories> and <pluginRepositories> say where dependencies and plugins are downloaded from, beyond Central, which the super POM supplies (the effective POM above shows it). Each repository has an id, a name, a url, a layout (default, or legacy for the Maven 1 layout) and independent releases and snapshots policies with enabled, updatePolicy (always, daily as the default, interval:X in minutes or never) and checksumPolicy (ignore, fail or warn, the default). A repository must follow the Maven repository layout; everything else, including mirrors and credentials in settings.xml, is on Repositories, Mirrors & Repository Managers and settings.xml & Password Encryption. Plain http repository URLs in examples older than Maven 3.8.1 are blocked by default today; see that page.

distributionManagement

<distributionManagement> is about the artifacts this project produces, not the ones it consumes: where deploy uploads them. The reference lists repository and snapshotRepository (an id, a name, a url and a layout; the id is matched to a <server> in settings.xml for credentials, and the snapshot repository is optional: with none, repository is used for snapshots), site (where site:deploy publishes the project site), downloadUrl, status and relocation.

Two notes on the elements. The upload URL is often not the download URL (an upload by HTTP PUT or scp, a download by HTTP GET), so the two are declared separately. And <status> must never be written by hand: Maven sets it when the artifact is transported to a repository, with the values none, converted, partner, deployed and verified, and "should never be configured in your pom.xml". The uploading itself, with real runs against a file:// repository, is on Install, Deploy & Distribution Management.

Relocation

When a project changes its coordinates, for example after a change of groupId, the old coordinates should keep resolving. The mechanism is a <relocation> element in the old coordinates' POM, published in the repository:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example.tides</groupId>
  <artifactId>tides-model</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>

  <distributionManagement>
    <relocation>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-model</artifactId>
      <message>The project moved to the io.tidepool group.</message>
    </relocation>
  </distributionManagement>
</project>

Only the elements that changed need to be given (version was left out, so it stays 1.0.0); the message explains why. In the run, this POM was installed into a scratch repository next to the real io.tidepool:tidepool-model:1.0.0, and a consumer declared a dependency on org.example.tides:tides-model:1.0.0. dependency:tree printed on 3.9.16, 3.10.0 and 4.0.0-rc-7 (release candidate), twice (once while the project was read, once for the plugin):

[WARNING] The artifact org.example.tides:tides-model:jar:1.0.0 has been relocated to io.tidepool:tidepool-model:jar:1.0.0: The project moved to the io.tidepool group.
[INFO] io.tidepool:tidepool-consumer:jar:1.0.0
[INFO] \- io.tidepool:tidepool-model:jar:1.0.0:compile

The consumer gets the new artifact and is told to update its POM. The two things people get wrong are where the element lives and what it does not do. It goes in the old coordinates' POM, the one that is published in the repository, and not in the new one. The related case of a third-party artifact that is not in any repository, and more on relocation, are on Installing Third-Party JARs & Relocation.

modelVersion 4.0.0 versus 4.1.0

modelVersion is the version of the POM format, not of the project. It has been 4.0.0 since Maven 2, and it stayed the only value through the whole of Maven 3. Maven 4 (release candidate) introduces model version 4.1.0, which has its own namespace, http://maven.apache.org/POM/4.1.0, and its own XSD, and adds elements such as <subprojects> (the new name of <modules>) and the bom packaging. The Maven 4 documentation also says that Maven 4 keeps building Maven 3’s 4.0.0 projects, so no one needs to move a POM to 4.1.0 unless a new feature is wanted. A minimal 4.1.0 POM:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
  <modelVersion>4.1.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-model</artifactId>
  <version>1.0.0</version>
</project>

mvn -B -ntp validate on this POM built on Maven 4.0.0-rc-7 (release candidate) and failed on both Maven 3 versions with the same message:

[INFO] Scanning for projects...
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[FATAL] 'modelVersion' of '4.1.0' is newer than the versions supported by this version of Maven: [4.0.0]. Building this project requires a newer version of Maven. @ line 5, column 17

Writing <modelVersion>4.1.0</modelVersion> inside a document that still uses the 4.0.0 namespace fails the same way on Maven 3.10.0, and was accepted by 4.0.0-rc-7. The consequence for a team is practical: a repository whose POMs say 4.1.0 can only be built by Maven 4, so the move is a decision for the whole team and the whole CI, and published artifacts are not affected because Maven 4 generates a consumer POM in the 4.0.0 format for them. What else changes, the inference of modelVersion and parent coordinates, and the migration steps are on Maven 4: POM Model & Consumer POM and Migrating to Maven 4. The XSD address of 4.1.0 currently redirects to a release candidate file of the model, and will change when 4.0.0 is final.

The same ideas in Ant and Gradle

Not every concept has an equivalent in the other two tools, which is itself a lesson about what a POM is. The table maps the parts of this page to Ant and to Gradle:

Concern Maven Ant Gradle

project identity

groupId, artifactId, version in the POM

only <project name="...">; a version is a property by convention

group and version of the project, rootProject.name in the settings (see Settings & Build Scripts)

properties

<properties>, overridden by -D

<property>; once set, a property is immutable, and -D values win over the file

gradle.properties, -P on the command line, extra properties (see Build Environment & Properties)

environment variables

${env.NAME}

<property environment="env"/> and ${env.NAME} (see Ant: Properties)

System.getenv() or the providers.environmentVariable provider

descriptive metadata (licenses, developers, scm)

elements of the POM

none

the pom { ... } block of a maven-publish publication, which writes a Maven POM

model version

modelVersion 4.0.0, 4.1.0 in Maven 4

not applicable

not applicable (the Gradle module metadata has its own version)

The idea shared by Gradle and Maven is that a published Maven POM is an interface to the rest of the world. Whatever builds the artifact, the consumers read the POM, which is why the Gradle maven-publish plugin generates one and why the rules for a self-contained POM in this page apply to it as well.

Classic mistakes

  • finalName does not rename the file in a repository, only the file in target.

  • A property named project.something is reserved. Maven 3.x ignores your value and Maven 4.0.0-rc-7 honours it.

  • A misspelt property name leaves ${name} literally in the value and fails later, in a place that looks unrelated, such as the dependency version message shown above. Compare the name in the message with the declaration.

  • ${pom.x} and ${x} are deprecated; use ${project.x}.

  • A POM published to a repository must not rely on -D, the environment or the consumer’s profiles. Maven 3.9.16 let a -D property leak into a dependency’s POM; Maven 3.10.0 does not.

  • The url and scm of a child get the artifactId appended. Set the child.*.inherit.append.path attributes in the parent when that is wrong.

  • ${maven.build.timestamp} is UTC wall-clock time and changes in every build. Use a fixed project.build.outputTimestamp for reproducible archives.

  • <prerequisites> in a jar project is only a warning; the build requirement belongs in the Enforcer plugin.

  • The relocation belongs in the old coordinates' POM.

  • A POM with modelVersion 4.1.0 cannot be built by Maven 3.

  • Never write <status> in a POM.

  • maven.compiler.source 1.7 is no longer accepted by the JDK, even when copied from official examples.

Where the books are outdated

The Maven 2 book that informed these pages predates most of what is on this page. Statements that no longer hold:

  • "POM model 4.0.0 only": Maven 4 adds model version 4.1.0, the consumer POM and the bom packaging (above).

  • A CI example based on Continuum: Continuum was retired in 2016 (it is in the Apache Attic). Use the CI system you have in ciManagement; the element is informational.

  • scm:svn: and scm:cvs: examples: the scm:<provider> format is unchanged, Git is the norm, and the release plugin works through Maven SCM.

  • Plain http repository URLs: blocked by default since Maven 3.8.1 (CVE-2021-26291); Central is HTTPS only.

  • Codehaus groupIds and URLs in examples: Codehaus closed, MojoHaus keeps org.codehaus.mojo as a groupId.

  • Plugins used without a version: the 3.10.0 super POM no longer manages the antrun, assembly, dependency and release plugins, and the versions of the packaging’s plugins come from the running Maven’s lifecycle bindings and change with it, so pin every one (see Plugins & Their Configuration).

  • Publishing through OSSRH: it was shut down on 2025-06-30; Central has a Publisher Portal.

Where to go next