Maven 4: What’s New

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 the overview of Apache Maven 4. Maven 4.0.0-rc-7 is a release candidate, not a final release: the release notes call it a version "intended for testing and feedback", say it is expected to be the last candidate before 4.0.0, and no 4.0.0 had been published when this page was written. Everything shown here can still change before the final release. The page is written against the 3.10.0 line (the current Maven 3 release, with 3.9.16 as the other supported one: see Maven Versions & Release History) and every Maven 4 example was run on 4.0.0-rc-7 on 2026-10-16, mostly on JDK 26 and once on JDK 17. The examples use the tidepool project (a parent POM with the tidepool-model, tidepool-engine and tidepool-cli subprojects) in a scratch directory, with a scratch local repository so that nothing touched ~/.m2. Paths were shortened to /work/..., and the lines Loaded N auto-discovered prefixes ... that rc-7 prints on the first request to each repository were filtered out; other trimming is marked in the text.

The page covers every item of the official "What’s new in Maven 4?" article, one short section each, and sends you to the sibling pages for depth: Maven 4: POM Model & Consumer POM, Maven 4: Lifecycle and Migrating to Maven 4.

The big idea: separate the build from the consumers

For twenty years the POM has served two audiences. Maven itself reads it to build the project (plugins, properties, profiles, modules). Everybody who depends on the artifact (other Maven projects, Gradle, IDEs, scanners, Maven Central) reads the very same file from the repository to learn its dependencies. Because the whole ecosystem parses that file, its schema, model version 4.0.0, could not change without breaking someone. Maven 4 breaks that deadlock by treating them as two documents: the build POM in your source tree may use the new model version 4.1.0, and what is installed and deployed for consumers is a consumer POM that still says 4.0.0. Most of what is new is either a consequence of that split or something the split now permits.

flowchart LR subgraph src["In source control"] B["Build POM, model 4.1.0 or 4.0.0"] end B -->|"Maven 4 builds the project"| R["Reactor, lifecycle, plugins"] B -->|"generated at build time"| C["Consumer POM, model 4.0.0"] B -->|"also kept as"| BP["Build POM with classifier build"] C -->|"install, deploy"| REPO[("Local or remote repository")] BP -->|"install, deploy"| REPO REPO -->|"read by"| E["Maven 3, Maven 4, Gradle, IDEs, scanners"]

The section Build POM and consumer POM below shows the files this produces.

At a glance

Area Maven 3.10.0 Maven 4.0.0-rc-7 Depth

Java to run Maven

Java 8 (3.9 and 3.10)

Java 17; the code you compile can still target an older release

Java 17 to run Maven

POM model

4.0.0 only

4.1.0 for the build POM (optional), 4.0.0 kept for consumers

Model version 4.1.0 and the 3.x error you will see

Multi-module

<modules>, a list of directories

<subprojects>; discovery without any list; parent and version inference

Subprojects instead of modules, Parent and version inference

Root directory

Convention (.mvn) plus internal properties

root="true" attribute and ${project.rootDirectory}, ${session.rootDirectory}, ${session.topDirectory}

The root directory and its properties

Source directories

<sourceDirectory>, <testSourceDirectory>

<build><sources> with scope, several directories, per-source options

Declaring source directories with sources

CI-friendly versions

${revision} plus the flatten plugin to publish

${revision} works for publishing with no plugin

CI-friendly versions without the flatten plugin

Resuming a failed build

-rf :module

-r / --resume, aware of already built subprojects

Reactor, resume and the end of half-deployed builds

Failing on warnings

Not available

-fos WARN / --fail-on-severity

Failing the build on log severity

Optional profiles

Missing profile: warning

Missing profile: error; -P?name restores the tolerant behaviour

Profiles: optional ids and condition activation

Lifecycle

Ordered list of phases, pre- and post- for some

Tree of phases, before: and after: for every phase, all and each

The lifecycle becomes a tree

Password encryption

mvn --encrypt-master-password, settings-security.xml

mvnenc (diag, init, encrypt, decrypt), vault support

Encryption, resolver, shell and upgrade tool

Upgrade help

None

mvnup check and mvnup apply

Encryption, resolver, shell and upgrade tool

Java 17 to run Maven

Maven 4 needs Java 17 or newer to run. Compiling for an older release is unchanged: you still choose it with the compiler plugin (maven.compiler.release) or a toolchain, see Compiling & Toolchains. rc-7 started on a JDK 17 (JetBrains Runtime) without problems:

$ JAVA_HOME=$HOME/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home mvn -v
Apache Maven 4.0.0-rc-7 (6d8f301b38af7399b36b862969d520974944f0b9)
Maven home: /work/tools/apache-maven-4.0.0-rc-7
Java version: 17.0.14, vendor: JetBrains s.r.o., runtime: /Users/example/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home

(The Default locale, OS name and later lines were left out.) Java 17 became the minimum with Maven 4.0.0-alpha-13, in March 2024. CI images that still run Maven on Java 8 or 11 must move; the project being built does not have to.

Model version 4.1.0 and the 3.x error you will see

A POM whose root element is declared in the namespace http://maven.apache.org/POM/4.1.0 is a model 4.1.0 POM. Maven 4 still builds every 4.0.0 POM unchanged, so nothing forces you to move. Maven 3 cannot read 4.1.0, and the failure is confusing, because it complains about the first new attribute it meets rather than about the version. This is the tidepool parent POM used below on Maven 3.10.0 (3.9.16 prints the same):

$ mvn verify
[INFO] Scanning for projects...
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[ERROR] Malformed POM /work/tidepool/pom.xml: Unknown attribute 'root' for tag 'project' (position: START_TAG seen ...ttps://maven.apache.org/xsd/maven-4.1.0.xsd"\n         root="true">... @5:22)  @ /work/tidepool/pom.xml, line 5, column 22
[ERROR] 'modelVersion' is missing. @ io.tidepool:tidepool-parent:${revision}, /work/tidepool/pom.xml, line 5, column 22
[ERROR] 'modelVersion' is missing. @ io.tidepool:tidepool-parent:${revision}, /work/tidepool/pom.xml, line 5, column 22
 @
[ERROR] The build could not read 1 project -> [Help 1]

(Only the first lines are shown.) The practical rule: the repository that holds a 4.1.0 build POM can only be built with Maven 4. What you publish does not have this problem, because consumers get the 4.0.0 consumer POM. The element-by-element list of what 4.1.0 adds is on Maven 4: POM Model & Consumer POM.

Subprojects instead of modules

Since Java 9 the word "module" also means a Java Platform Module System module, and a Maven "module" is merely a project in a subdirectory. Maven 4 therefore renames the concept: model 4.1.0 has a <subprojects> element (with <subproject> children) analogous to <modules>, which stays usable but is deprecated. A project with subprojects is a multi-project build, one without is a single-project build. The reactor itself is unchanged, see Multi-Module Builds & the Reactor.

The tidepool parent POM below has no list at all:

<?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"
         root="true">
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-parent</artifactId>
  <version>${revision}</version>
  <packaging>pom</packaging>

  <properties>
    <revision>1.0.0-SNAPSHOT</revision>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <!-- build/pluginManagement with every plugin version pinned: 3.16.0 compiler, 3.5.1 jar, ... -->
</project>

When a parent has pom packaging and neither <subprojects> nor <modules>, Maven 4 adds every direct subdirectory that contains a pom.xml. Each subproject below inherits and depends without repeating coordinates (explained in Parent and version inference):

<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">
  <parent/>
  <artifactId>tidepool-engine</artifactId>
  <dependencies>
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-model</artifactId>
    </dependency>
  </dependencies>
</project>

Running mvn clean verify on rc-7 found all three subprojects and ordered them from the dependencies:

$ mvn clean verify
[INFO] Scanning for projects...
[INFO] Reactor Build Order:
[INFO] tidepool-parent                                                    [pom]
[INFO] tidepool-model                                                     [jar]
[INFO] tidepool-engine                                                    [jar]
[INFO] tidepool-cli                                                       [jar]
[INFO] Building tidepool-parent 1.0.0-SNAPSHOT                            [1/4]
[INFO]   from pom.xml
...
[INFO] tidepool-parent .................................... SUCCESS [  0.645 s]
[INFO] tidepool-model ..................................... SUCCESS [  0.694 s]
[INFO] tidepool-engine .................................... SUCCESS [  0.096 s]
[INFO] tidepool-cli ....................................... SUCCESS [  0.045 s]

(Only the reactor lines are shown; the ... marks the removed plugin output.) The same discovery is also what makes <parent/> find the parent: with an empty <parent/> Maven looks for a pom.xml in ...

Parent and version inference

Maven 4 finally ships MNG-624, opened in 2005: with model 4.1.0 you need not repeat the parent version, nor the version of a dependency on another project of the same reactor. You can write <parent><relativePath>..</relativePath></parent>, the shorthand <parent/> (parent in ..), or just leave out <version>, <groupId> or <artifactId> of the parent. A dependency on a sibling needs only groupId and artifactId. The tidepool-engine POM above does exactly this, and the dependency on tidepool-model has no version. The result is that a version bump is a change in one file.

The inferred values are written back into the consumer POM, so readers never see the shorthand (see Build POM and consumer POM).

CI-friendly versions without the flatten plugin

Maven 3.5.0 added ${revision}, ${sha1} and ${changelist} as version placeholders, but publishing a POM that still contained the placeholder broke consumers, so everybody added the Flatten Maven Plugin. In Maven 4 the consumer POM is generated with the placeholder replaced, and any property name works, not just the three predefined ones. The tidepool parent above has <version>${revision}</version> and no flatten plugin; mvn install on rc-7 produced a consumer POM with the real version (see the next section). Define the property on the command line (-Drevision=4.0.1), in .mvn/maven.config, or in the root or parent POM. The full treatment, including the Maven 3 workflow it replaces, is on Maven CI-Friendly Versions.

Build POM and consumer POM

mvn install on rc-7 put four files for each subproject into the scratch local repository (shown here for tidepool-engine):

tidepool-engine-1.0.0-SNAPSHOT-build.pom
tidepool-engine-1.0.0-SNAPSHOT.jar
tidepool-engine-1.0.0-SNAPSHOT.pom
maven-metadata-local.xml

(_remote.repositories was left out.) The -build.pom is the file from your source tree, installed with the classifier build, and it is the only one that is model 4.1.0. The plain .pom, the one every consumer reads, is generated:

<?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>io.tidepool</groupId>
    <artifactId>tidepool-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>tidepool-engine</artifactId>
  <dependencies>
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-model</artifactId>
      <version>1.0.0-SNAPSHOT</version>
    </dependency>
  </dependencies>
</project>

The model version is 4.0.0, the <parent/> shorthand has become a real parent with a version, and the dependency has its version. By default the consumer POM keeps the parent reference, which is why the parent POM is installed too. Setting the user property maven.consumer.pom.flatten=true goes further and produces a flattened POM. The same build with -Dmaven.consumer.pom.flatten=true:

<?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>io.tidepool</groupId>
  <artifactId>tidepool-engine</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-model</artifactId>
      <version>1.0.0-SNAPSHOT</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

The parent is gone and the group and version are taken from it. Flattening is off by default because it changes which dependencies are listed (only compile and runtime ones, and managed versions only when used); the official comparison table says that the consumer POM keeps third-party dependency information, repository information, project information and the deployment information, while POM properties and plugin configuration exist only in the build POM. Where the consumer POM cannot be expressed in 4.0.0 (a condition-based profile, see Profiles: optional ids and condition activation), Maven warns and may publish a 4.1.0 consumer POM. Pitfalls and the Mermaid flow of how the model builder produces each file are on Maven 4: POM Model & Consumer POM.

The root directory and its properties

Maven always needed to know where the project root is (to find .mvn/maven.config, the parent, shared files). Until now the only way to tell it was a .mvn directory, and there was no official property to refer to that directory from a POM (people used internal properties instead, now removed or deprecated, see MNG-7038). Maven 4 adds a boolean root attribute on <project> (model 4.1.0 only; default false), and three properties:

Property Scope Value Always set

${project.rootDirectory}

Project

The directory with .mvn or with the root="true" POM

No

${session.topDirectory}

Session

The directory Maven was started in, or the one holding the -f file

Yes

${session.rootDirectory}

Session

The root directory of the top project

No

${basedir} is the base directory of each individual subproject, as before. When no root can be found rc-7 does not stay silent: on a single-project build without .mvn and without the attribute, it prints this warning on the first line of output:

[WARNING] Unable to find the root directory. Create a .mvn directory in the root directory or add the root="true" attribute on the root project's model to identify it.

The new plugin generation depends on it. The 4.x compiler plugin refuses to run without a root (see Declaring source directories with sources), so a .mvn directory or root="true" is part of the move to Maven 4 even for projects that do not need the properties.

Declaring source directories with sources

Maven 3 has <sourceDirectory> and <testSourceDirectory>, one directory each, so a second source root needed the Build Helper plugin. Maven 4 adds <build><sources>, a repeatable <source> with a <scope> (main or test), a <directory> and, in the compiler plugin’s documentation, include and exclude filters and per-source target releases for multi-release projects and the module source hierarchy. A scratch project with two main source roots:

<build>
  <sources>
    <source>
      <scope>main</scope>
      <directory>src/main/java</directory>
    </source>
    <source>
      <scope>main</scope>
      <directory>src/extra/java</directory>
    </source>
  </sources>
</build>

With the 3.x compiler plugin line, 3.16.0, both directories were compiled (Compiling 2 source files). The 4.x line, 4.0.0-beta-5, which is the first to read the new element natively, produced both classes (A.class and Extra.class) too, but only after the build had a root directory. In the same project without root="true" (and without .mvn):

[INFO] --- compiler:4.0.0-beta-5:compile (default-compile) @ srcs ---
[ERROR] COMPILATION ERROR: Unable to find the root directory. Create a .mvn directory in the root directory or add the root="true" attribute on the root project's model to identify it.
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE

Note that the 4.x plugin lines (compiler 4.0.0-beta-5, install and deploy 4.0.0-beta-3, resources 4.0.0-beta-2) are the ones that use the new Maven API, and they only run on Maven 4.

New artifact types and the bom packaging

A dependency’s <type> selects the artifact kind. jar stays the default, and Maven 4 adds types that say where the JAR goes:

Type Meaning

jar

Class path or module path, decided heuristically (unchanged)

classpath-jar

Always the class path

modular-jar

Always the module path

processor

Annotation processor path, class path or module path decided heuristically

classpath-processor

Always the annotation processor class path

modular-processor

Always the annotation processor module path

The article warns that, in October 2025, only the compiler plugin (4.0.0-beta-3 and later) honours them; other plugins will follow.

Maven 4 also adds a dedicated bom packaging, so a bill of materials is no longer a parent POM in disguise. It exists only in the build POM (model 4.1.0); Maven generates a Maven 3 compatible consumer POM. This POM, built with mvn install on rc-7 (the .mvn directory is present, and the install plugin version was not pinned):

<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">
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-bom</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>bom</packaging>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.tidepool</groupId>
        <artifactId>tidepool-model</artifactId>
        <version>1.0.0-SNAPSHOT</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

installed this consumer POM, in which bom became 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>io.tidepool</groupId>
  <artifactId>tidepool-bom</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.tidepool</groupId>
        <artifactId>tidepool-model</artifactId>
        <version>1.0.0-SNAPSHOT</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

Two related details from the article: in model 4.1.0 an import-scope BOM can be selected with <classifier> and its dependencies can be excluded with <exclusions>, and a BOM imported from the same reactor draws a warning, because the team recommends importing only BOMs that are published before the build. For the Maven 3 mechanics see Dependency Management & BOMs.

Alternate POM syntaxes

The 4.0.0 consumer POM must stay XML, but the build POM may not have to. Maven 4 has a ModelParser service provider interface (MNG-7836) that a core extension can implement to read the build POM in another syntax. The article names the Apache Maven Hocon Extension as one of the first users. Core extensions are loaded from .mvn/extensions.xml, see Maven Extensions. Nothing in this page was run against a custom parser.

Reactor, resume and the end of half-deployed builds

Resume. In Maven 3, a failing subproject B that depends on A produced the advice to resume from B, which then failed at once because A was no longer built. In Maven 4, -r or --resume resumes from the subproject that failed last time, and the reactor remembers the subprojects that succeeded. The reactor summary of a failing build points at it. A Broken.java was added to tidepool-cli:

$ mvn clean verify
...
[INFO] tidepool-parent .................................... SUCCESS [  1.634 s]
[INFO] tidepool-model ..................................... SUCCESS [  6.406 s]
[INFO] tidepool-engine .................................... SUCCESS [  0.101 s]
[ERROR] tidepool-cli ....................................... FAILURE [  0.045 s]
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[ERROR] /work/tidepool/tidepool-cli/src/main/java/io/tidepool/cli/Broken.java:[3,24] incompatible types: java.lang.String cannot be converted to int
...
[ERROR] After correcting the problems, you can resume the build with the command
[ERROR]   mvn [args] -r

After deleting the file, mvn -r verify (the output was filtered to the relevant lines):

$ mvn -r verify
[INFO] Resuming from io.tidepool:tidepool-cli due to the --resume / -r feature.
[INFO] BUILD SUCCESS

Maven 3.10.0 does not know the option: mvn -r verify ends with Unable to parse command line options: Unrecognized option: -r. The long-standing bug that made -am ignore the resumed subprojects (MNG-6863) is fixed, and the reactor is aware of builds started from a subdirectory (MNG-6118). The article’s advice follows from this: use mvn verify for everyday builds, not mvn clean install.

All or nothing deploy. In Maven 3, a failure late in the reactor left the early subprojects installed or deployed. The article says Maven 4 changes the deployAtEnd default to true. That default is a property of the plugin, not of the core, and the experiment showed it: with the 3.x install and deploy plugins (3.2.0) pinned, the failing build above still installed tidepool-parent, tidepool-model and tidepool-engine into a fresh local repository, and the mvn deploy to a file:// repository (with -DaltDeploymentRepository) wrote 42 files before tidepool-cli failed. With the 4.x plugin line (4.0.0-beta-3 for install and deploy) in the parent:

[INFO] tidepool-parent .................................... SUCCESS [  1.411 s]
[INFO] tidepool-model ..................................... SUCCESS [  4.788 s]
[INFO] tidepool-engine .................................... SUCCESS [  0.104 s]
[ERROR] tidepool-cli ....................................... FAILURE [  0.036 s]
[ERROR] BUILD FAILURE

and neither the scratch local repository nor the file:// repository received a single file. On success the same plugins installed all four subprojects after the last jar goal (Installing ... lines come after install:4.0.0-beta-3:install of the last subproject). So "deploy only if everything built" needs both Maven 4 and the 4.x install and deploy plugin versions: pin them in pluginManagement. Section Where install, deploy and release are covered says where this is treated in depth.

Further, all subprojects now get the same timestamp in their SNAPSHOT file names (MNG-6754).

Where install, deploy and release are covered

The mechanics of install, deploy and distributionManagement are on Install, Deploy & Distribution Management and the release process on Releasing. The release-candidate status of Maven 4 matters there: the release plugin line for Maven 4 was not part of this page’s runs, so use Maven 3.10 or 3.9 for releases until you have tested your own flow.

Failing the build on log severity

Warnings that nobody reads become errors with -fos WARN (long form --fail-on-severity WARN). The argument may be WARN, WARNING or ERROR; any other value is rejected. A project whose POM does not pin the compiler and resources plugins gets a warning on rc-7:

$ mvn compile -fos WARN
...
[WARNING] Version not locked for default bindings plugins [maven-resources-plugin, maven-compiler-plugin], you should define versions in pluginManagement section of your pom.xml or parent
[INFO] --- resources:3.5.0:resources (default-resources) @ warny ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ warny ---
[INFO] Nothing to compile - all classes are up to date.
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[ERROR] Build failed due to log statements with a higher severity than allowed. Fix the logged issues or remove flag --fail-on-severity (-fos).

The exit code was 1; with -fos ERROR the same build succeeded with exit code 0. A bad value:

[ERROR] Error executing Maven.
[ERROR] Invalid fail on severity threshold 'fatal'. Supported values are 'WARN', 'WARNING' and 'ERROR'.

The warning shown is the other point of the super POM upgrade: its default plugin versions are new in Maven 4, so a build that relied on them may change behaviour, and rc-7 now warns that you do. Pin every plugin version. Maven 3.10.0 has no such option: --fail-on-severity is Unrecognized option, and the short -fos WARN is silently read as -f os WARN, the file os, with the error Non-readable POM /work/classic/os. This option is the quickest way to keep a Maven 4 migration clean in CI (see Migrating to Maven 4).

Profiles: optional ids and condition activation

A missing profile is now an error. In Maven 3 -Pnonexistent printed a warning and carried on, which hid typos. Maven 4 stops. Run on the same 4.0.0 project with the three releases (Maven 3.9.16 and 3.10.0 print the same):

$ mvn compile -Pnonexistent     # Maven 3.9.16 and 3.10.0
[WARNING] The requested profile "nonexistent" could not be activated because it does not exist.
[INFO] BUILD SUCCESS

$ mvn compile -Pnonexistent     # Maven 4.0.0-rc-7
[ERROR] The requested profiles [nonexistent] could not be activated or deactivated because they do not exist. -> [Help 1]
...
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MissingProfilesException

(The Maven 3 warning is printed twice, at the start and at the end of the build; one is shown.) Prefix the id with ? to get the tolerant behaviour back. The command needs quoting in shells where ? is a wildcard:

$ mvn compile '-P?nonexistent'
[INFO] The requested optional profiles [nonexistent] could not be activated or deactivated because they do not exist.
...
[INFO] BUILD SUCCESS
...
[INFO] The requested optional profiles [nonexistent] could not be activated or deactivated because they do not exist.

Condition-based activation. In model 4.1.0 <activation> accepts a <condition> element holding an expression. The functions present in the rc-7 implementation are length, upper, lower, substring, indexOf, contains, matches, not, if, exists, missing and inrange, with the logical operators of the expression language. An unknown function is a model error:

[ERROR]     Error parsing profile activation condition: Unknown function: lessThan @ /work/cond/pom.xml

(Only the first of two identical lines is shown.) A profile that is active when a data directory exists and the running Java is 17 or newer:

<profiles>
  <profile>
    <id>on-jdk-17-plus-with-data</id>
    <activation>
      <condition>exists("${project.basedir}/data") &amp;&amp; inrange(${java.specification.version}, "[17,)")</condition>
    </activation>
    <properties>
      <flavour>with-data</flavour>
    </properties>
  </profile>
</profiles>

With and without the directory (flavour has the default plain in <properties>):

$ mvn help:active-profiles help:evaluate -Dexpression=flavour -DforceStdout     # with a data/ directory
The following profiles are active:

 - on-jdk-17-plus-with-data (source: io.tidepool:cond:1.0.0-SNAPSHOT)

with-data

$ rmdir data
$ mvn help:evaluate -Dexpression=flavour -q -DforceStdout
plain

(Both runs filtered to the relevant lines.) Maven 3.10.0 rejects the same POM with Malformed POM ... Unrecognised tag: 'condition'. The profile reference is on Maven Profiles. A profile that uses <condition> cannot be written in 4.0.0, so the published consumer POM may be 4.1.0 (Maven warns when that happens).

The lifecycle becomes a tree

The Maven 3 lifecycle is one ordered list of phases. Maven 4 defines it as a tree, which allows finer dependencies (a project can start once a dependency has reached the ready phase instead of finishing verify) and skipping steps (deploy without installing). For compatibility the default execution does not change; the tree is used by the opt-in builder: mvn -b concurrent .... Every phase has a before: and an after: hook, executions in the same phase can be ordered with [n], and two new phases, all and each, wrap a whole project and each phase of it. The deprecated pre-* and post-* names remain as aliases.

A scratch plugin binding to try the hooks, in tidepool-model (the antrun plugin with <echo>):

<execution>
  <id>hook-before-compile</id>
  <phase>before:compile</phase>
  <goals><goal>run</goal></goals>
  <configuration><target><echo message="before:compile"/></target></configuration>
</execution>
<execution>
  <id>hook-after-compile-200</id>
  <phase>after:compile[200]</phase>
  <goals><goal>run</goal></goals>
  <configuration><target><echo message="after:compile[200]"/></target></configuration>
</execution>
<!-- plus after:compile[100], before:all and after:all, written the same way -->

mvn compile -pl tidepool-model on rc-7 (plugin goals only):

[INFO] --- antrun:3.1.0:run (hook-before-all) @ tidepool-model ---
[WARNING]      [echo] before:all
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-model ---
[INFO] --- antrun:3.1.0:run (hook-before-compile) @ tidepool-model ---
[WARNING]      [echo] before:compile
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-model ---
[INFO] --- antrun:3.1.0:run (hook-after-compile-100) @ tidepool-model ---
[WARNING]      [echo] after:compile[100]
[INFO] --- antrun:3.1.0:run (hook-after-compile-200) @ tidepool-model ---
[WARNING]      [echo] after:compile[200]
[INFO] --- antrun:3.1.0:run (hook-after-all) @ tidepool-model ---
[WARNING]      [echo] after:all

(The [WARNING] prefix is how the antrun plugin prints its <echo>.) Two behaviour changes follow from the new hooks. First, Maven 4 always runs the after: hook of a phase it reaches, while Maven 3 ran a post-* phase only when the next phase ran or when you named it. The experiment bound the echo to post-clean in a 4.0.0 POM: mvn clean -pl tidepool-model printed nothing from it on Maven 3.10.0 and printed post-clean ran on rc-7. Second, binding to a pre-* or post-* phase now works as an alias, but the article warns against it, notably for plugins bound to a post-* phase whose pre-* counterpart does not exist.

An observation specific to rc-7 and the concurrent builder: with -b concurrent, the run of the same compile printed before:all, before:compile, the compiler and after:all, but neither resources:resources nor the two after:compile[n] executions. The build still reported success. Treat -b concurrent as experimental in the release candidate and compare the executed goals with those of the default builder before relying on it. An unknown builder id fails with BuilderNotFoundException. The phases, ordering and builder are explained on Maven 4: Lifecycle, and the Maven 3 lifecycle on Maven Build Lifecycle.

Plugins: what stops working

Maven 4 removes long-deprecated pieces, and a plugin that uses them stops working:

  • Plexus dependency injection. "Plexus Containers" injection, deprecated since Maven 3.2 (2010), is gone. A plugin that still depends on it needs JSR-330 (javax.inject). rc-7 adds validation for it. The distribution still carries the Sisu Plexus bridge (org.eclipse.sisu.plexus-1.1.0.jar), the same as 3.10.0, and plexus-component-annotations-2.2.0.jar.

  • Maven 2-era APIs. Plugins that never moved to the Maven 3 APIs are the ones at risk. Maven 4 is expected to run any plugin that follows Maven 3 best practices. Run -Dmaven.plugin.validation=verbose on 3.9.x or 4 to list what a plugin does wrong.

  • The ${pom.*} expressions. The article says these were removed, and Maven 3.9.16 does warn about them (The expression ${pom.version} is deprecated. Please use ${project.version} instead.). In an experiment with <label>${pom.artifactId}-${pom.version}</label> in a 4.0.0 POM, rc-7 resolved the value (pomexp-1.0.0-SNAPSHOT) with no warning at all, so do not count on rc-7 to find them for you; search your POMs for ${pom., and use ${project..

  • The new plugin API. A plugin API on the immutable plugin model exists, but is still experimental in 4.0.0: only the 4.x compiler plugin is a reason to use it today.

Writing plugins is on Writing Maven Plugins.

Encryption, resolver, shell and upgrade tool

mvnenc and password encryption. Maven 3’s master-password mechanism was "obfuscation". Maven 4 replaces it with Maven Encryption, a command line tool mvnenc, which has these goals in rc-7:

Goals:
  diag - display encryption configuration diagnostic
  init - wizard to configure encryption (interactive only)
  encrypt - encrypts input
  decrypt - decrypts encrypted input

It adds decrypt and supports external vaults. The command is also reachable as mvn --enc. The settings that use encrypted values are on settings.xml & Password Encryption; mvnenc init is interactive and was not run here.

Maven Resolver 2. The core ships Maven Artifact Resolver 2.0.23 (maven-resolver-impl-2.0.23.jar); Maven 3.10.0 ships 2.0.24, so Resolver 2 is not new compared with 3.10. What is new is that in Maven 4 the resolver is behind the Maven API and plugins do not call it directly. The rc-7 log line Loaded 24114 auto-discovered prefixes for remote repository central comes from it. See Maven Resolver.

mvnsh, the Maven Shell. mvnsh keeps one Maven process open for a session, which saves the JVM start and configuration load of every command. It is the single-process counterpart to the Maven Daemon (mvnd), see Build Performance: Cache, Parallel Builds & mvnd. Run without a terminal (printf — '-v\nexit\n' | mvnsh), it printed its banner and 4.0.0-rc-7 and then ended with a NullPointerException from the JLine terminal capability lookup, so the shell needs a real interactive terminal. It was not run interactively here.

mvnup, the Upgrade Tool. mvnup check reports what needs to change, and mvnup apply rewrites the POMs. --all (-a) means --model-version 4.1.0 --infer --model --plugins. On a copy of the 4.0.0 tidepool project (committed in a scratch git repository), mvnup -B check --all listed 13 strategies (excerpt):

[INFO] Maven Upgrade Tool - Check
[INFO] Found 4 POM file(s)
[INFO] Executing 13 upgrade strategy(ies):
[INFO]   - Upgrading POM model version
[INFO]   - Applying Maven inference optimizations
[INFO]   - Removing duplicate XML elements
[INFO]   - Applying Maven 4 compatibility fixes
[INFO]   - Migrating source configuration to <source> elements
[INFO]   - Deduplicating dependency and plugin declarations
[INFO]   - Injecting standalone Nashorn for antrun JavaScript compatibility
[INFO]   - Widening RequireMavenVersion ranges to allow Maven 4
[INFO]   - Adding binary file extension exclusions to resource filtering
[INFO]   - Upgrading HTTP repository URLs to HTTPS
[INFO]   - Adding maven-toolchains-plugin for JDK source level compatibility
[INFO]   - Fixing invalid Automatic-Module-Name manifest entries
[INFO]   - Upgrading Maven plugins to recommended versions
...
[INFO]         • Converted <modules> to <subprojects>
[INFO]         • Upgraded phase: post-clean → after:clean

mvnup -B apply --all then changed all four POMs (git diff --stat: 17 insertions, 32 deletions): namespace and schema to 4.1.0, <modules> to subprojects (or auto-discovery), parents shortened to the <parent> shorthand, post-clean to after:clean, and maven.compiler.release to a <sources><source><targetVersion>17</targetVersion></source></sources> block. That last change shows the one trap worth knowing: tidepool pins the 3.x compiler plugin 3.16.0, which does not read <sources>, so mvn verify on the upgraded project failed:

[ERROR] /work/up/tidepool-model/src/main/java/io/tidepool/model/Tide.java:[3,8] records are not supported in -source 8
[ERROR]   (use -source 16 or higher to enable records)

Review the diff and re-run the build after mvnup apply; with the 3.x compiler plugin keep maven.compiler.release, or move to the 4.x compiler plugin. The release notes of rc-7 list several mvnup fixes, so the tool is itself still moving. The step-by-step migration is on Migrating to Maven 4.

Equivalents in Ant and Gradle

Maven 4’s separation of build and published metadata is the model Gradle already has: its Gradle Module Metadata is published next to a POM and the build script is never published (see Comparing Ant, Maven & Gradle). Subprojects are the Gradle word as well. Apache Ant has neither a project model nor a lifecycle, so there is nothing to compare for the lifecycle tree. The migration page between tools covers moving between them.

What to do while 4.0.0 is a release candidate

  • Try your builds on rc-7 in CI next to the production Maven, and treat failures as feedback to the project (GitHub issues).

  • Keep the build POM at 4.0.0 until you need a 4.1.0 feature: a 4.0.0 POM builds on Maven 3.9, 3.10 and 4, and the consumer POM is the same 4.0.0 either way. Moving to 4.1.0 is a one-way step for contributors who still run Maven 3.

  • Pin every plugin version, add a .mvn directory or root="true", and use -fos WARN once to see what the build is warning about.

  • Use the 4.x plugin lines only on Maven 4 builds.

  • Do not depend on the -b concurrent builder or on mvnsh for production yet.

The complete list of resolved issues is the issue overview linked from the article.