Maven Multi-Module Builds & the Reactor

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. Every example was run on JDK 26 on 2026-10-16, with Maven 3.9.16 and Maven 4.0.0-rc-7 (a release candidate) run wherever the page names them. All builds used a scratch local repository, so nothing was installed into the real one, and deployments went to a scratch directory addressed with file://. Absolute paths in console output were shortened to ...; log lines that were left out are marked, and the timings of the reactor summary were removed unless they are the point of the example. Plugin versions are the stable releases found on Maven Central on 2026-10-16.

A real application is rarely one JAR. It is a model, an engine on top of it, a command line, a report generator, and a parent that holds what they share. Maven builds such a set of projects with one command, and the part of Maven that does it is the reactor. The reactor decides which projects take part, in which order they are built, which of them are built at all, and what happens to the rest when one fails. It also decides where a module finds the artifacts of its sibling modules, which is the source of most surprises: a build that works from the root and fails from a module directory, or one that picks up a stale JAR from a previous build.

This page is about the reactor: ordering, the selection flags (-pl, -am, -amd, -rf), failure and resume policies, and reactor resolution. How a POM declares its modules and how a child inherits from a parent is the subject of Inheritance, Aggregation & the Effective POM and is not repeated here. The command-line basics are on Running Maven from the Command Line, and the phases that each module goes through are on Build Lifecycle.

What the reactor does

The Maven guide for multiple modules describes the reactor as the part of the Maven core that collects all the available modules to build, sorts them into the correct build order, and builds the selected projects in order. The guide for Maven 4 adds a step, select, between sorting and building, and the flags of this page act exactly there. Each step is a place where something can go wrong, and each has its own error message.

graph LR A["Collect: read the aggregator POM, then every module POM, recursively"] --> B["Sort: order by dependencies, parents and plugins"] B --> C["Select: apply -pl, -am, -amd, -rf, -N"] C --> D["Build: run the requested phases module by module"] D --> E["Summary: SUCCESS, FAILURE or SKIPPED per project"]
  • Collect. Maven reads the POM in the working directory. If it lists <modules>, Maven reads each module’s POM, and so on down the tree. A module directory that does not exist is an error at this step, before anything is built.

  • Sort. The collected projects form a directed graph, and Maven orders it so that a project is built after everything it needs. A cycle is an error at this step.

  • Select. The command-line flags remove projects from the sorted list. The sort order of what remains is not changed.

  • Build. For each remaining project in order, Maven runs the phases or goals given on the command line, and records the result.

The example build

The examples use a scratch project named tidepool, a small tide prediction toolkit, as in Inheritance, Aggregation & the Effective POM, extended with a fourth module and with tests so that the reactor has something to do. There are five projects:

tidepool/                  pom.xml           io.tidepool:tidepool-parent   pom, parent and aggregator
  tidepool-model/          pom.xml           io.tidepool:tidepool-model    jar, no dependencies on siblings
  tidepool-engine/         pom.xml           io.tidepool:tidepool-engine   jar, depends on tidepool-model
  tidepool-cli/            pom.xml           io.tidepool:tidepool-cli      jar, depends on tidepool-engine
  tidepool-report/         pom.xml           io.tidepool:tidepool-report   jar, depends on tidepool-model

The parent inherits from a company parent org.example.corp:example-corporate-parent:7 that lives in a sibling directory and pins the plugin versions (compiler 3.16.0, jar 3.5.1, surefire 3.6.0) and the JUnit version. The module list is deliberately not in dependency order:

  <modules>
    <module>tidepool-cli</module>
    <module>tidepool-engine</module>
    <module>tidepool-model</module>
    <module>tidepool-report</module>
  </modules>

This module list is this page’s own lab setup, not an example from the Maven documentation.

The parent’s dependencyManagement lists the sibling artifacts with ${project.version} as the version, so a module declares tidepool-model without a version. The model module publishes its test classes as a second JAR (more on this in Sharing test code with a test-jar), and the engine module depends on both JARs:

    <!-- tidepool-model/pom.xml, build section -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <executions>
        <execution>
          <goals>
            <goal>test-jar</goal>
          </goals>
        </execution>
      </executions>
    </plugin>

    <!-- tidepool-engine/pom.xml, dependencies section -->
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-model</artifactId>
    </dependency>
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-model</artifactId>
      <version>${project.version}</version>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>

tidepool-cli depends on tidepool-engine, and tidepool-report depends on tidepool-model only. The graph below is the dependency graph the reactor sorts. The parent is part of it: every module needs its parent, so the parent comes first.

graph TD P["1 tidepool-parent - pom"] M["2 tidepool-model"] E["3 tidepool-engine"] C["4 tidepool-cli"] R["5 tidepool-report"] P --> M M --> E E --> C M --> R classDef sel fill:#cfe8ff,stroke:#1f5fa8,color:#102a43 class P,M,E,C sel

The numbers are the position in the reactor build order, which the next section reproduces. The highlighted projects are the ones that -pl tidepool-cli -am selects, as the section on selection shows. tidepool-report is not an ancestor of tidepool-cli, so it stays out.

Reactor sorting

The guide lists the relationships the reactor honours when it sorts the projects:

  • a project dependency on another module in the build,

  • a plugin declaration where the plugin is another module in the build,

  • a plugin dependency on another module in the build,

  • a build extension declaration on another module in the build,

  • the order declared in <modules>, if no other rule applies.

The parent relation also counts, as the tidepool build shows. Only instantiated references count: entries in dependencyManagement and pluginManagement do not change the order, which is why the parent can list every sibling in its dependencyManagement without becoming a dependent of them.

mvn validate on tidepool (Maven 3.10.0; 3.9.16 and 4.0.0-rc-7 print the same order) shows the result:

Reactor Build Order:

tidepool                                                           [pom]
tidepool-model                                                     [jar]
tidepool-engine                                                    [jar]
tidepool-cli                                                       [jar]
tidepool-report                                                    [jar]

(The [INFO] prefixes were removed.) The <modules> list is cli, engine, model, report, yet the model comes before the engine and the engine before the CLI: the dependencies decided that. The last rule decided between the two projects the dependencies leave open: tidepool-cli and tidepool-report are independent, and tidepool-cli, which comes first in <modules>, is built first. In each module, the log then shows its position as [2/5] in the Building line. The first source of confusion for newcomers is that this order has nothing to do with the order in the POM or with the alphabet.

Three of the sorting rules were not run here: a plugin that is itself a module of the build (the plugin must be built before the module that uses it), a plugin dependency that is a module, and a build extension that is a module. They follow the guide quoted above; a scratch Maven plugin project was not built for this page.

Cycles

If the dependencies form a cycle there is no valid order. Adding a dependency of tidepool-model on tidepool-engine to a scratch copy makes the build stop in the sort step, before any plugin runs. Maven 4.0.0-rc-7 printed:

[ERROR] The projects in the reactor contain a cyclic reference: Edge between 'io.tidepool:tidepool-model:1.0.0' and 'io.tidepool:tidepool-engine:1.0.0' introduces to cycle in the graph io.tidepool:tidepool-engine:1.0.0 --> io.tidepool:tidepool-model:1.0.0 --> io.tidepool:tidepool-engine:1.0.0

Maven 3.10.0 printed the same message, but wrapped each project in Vertex{label='...'}. The usual cause is not a design error but a convenience: a module that needs a few classes from a sibling that depends on it. The fix is a new module that holds the shared classes, as tidepool-model does here. Test code is the exception that has a standard solution, see Sharing test code with a test-jar.

A module that does not exist

A <module> entry that names a missing directory fails at the collect step. A scratch copy that renamed tidepool-report to tidepool-reports in the module list printed (Maven 3.10.0):

[ERROR] Some problems were encountered while processing the POMs:
[ERROR] Child module .../tidepool-reports of .../pom.xml does not exist @
[ERROR] The build could not read 1 project -> [Help 1]

Reading the reactor output

A reactor build writes the same three things every time, and knowing them makes failures readable.

  1. Reactor Build Order at the start, with the packaging of every project in brackets.

  2. For each project a header Building tidepool-engine 1.0.0 with its position [3/5], and the from line with the POM file. The position is the order in which the projects start, so in a parallel build it is not the order in which they end.

  3. The Reactor Summary at the end, with one line per project (SUCCESS, FAILURE or SKIPPED) and the time spent.

A project that is built alone, for example with -pl :tidepool-model, is not a reactor build for the log: Maven prints no Reactor Build Order and no summary, only the single Building header.

In a build with failures, the summary of Maven 3.10.0 and Maven 4.0.0-rc-7 was grouped by result, with the skipped projects first and the failed one last, while Maven 3.9.16 printed the projects in build order. The failed run below shows both.

Selecting projects

By default Maven builds everything the reactor collected. The flags below shrink that set. The options table lists what mvn -h printed for Maven 3.10.0.

Option Meaning Since or note

-pl, --projects

Comma-delimited list of reactor projects to build instead of all projects. A project can be specified by [groupId]:artifactId or by its relative path.

Maven 3; selectors with +, -, ! and ? prefixes in Maven 4

-am, --also-make

If a project list is specified, also build the projects required by the list.

Maven 3

-amd, --also-make-dependents

If a project list is specified, also build the projects that depend on the projects on the list.

Maven 3

-N, --non-recursive

Do not recurse into sub-projects.

Maven 3; with -pl it prevents the selection of children

-rf, --resume-from

Resume the reactor from the specified project.

Maven 3

-r, --resume

Resume the reactor from the last failed project, using the resume.properties file.

Maven 4

-ff, --fail-fast

Stop at the first failure in reactorized builds. This is the default.

Maven 3

-fae, --fail-at-end

Only fail the build afterwards; allow all non-impacted builds to continue.

Maven 3

-fn, --fail-never

Never fail the build, regardless of the project result.

Maven 3

-T, --threads

Thread count, for instance 4 or 2C/2.5C where C is the number of cores.

Maven 3

-b, --builder

The id of the build strategy to use.

Maven 3

-pl: choosing projects

Each -pl entry is a selector, and there are three forms:

  • tidepool-model: the artifactId alone, which works as long as the name is unique in the reactor,

  • :tidepool-model or io.tidepool:tidepool-model: the colon form, with or without the groupId,

  • ./tidepool-cli: the relative path of the module directory, which also works for a module whose artifactId is not unique.

Maven 3.10.0 built the three projects of -pl :tidepool-model,./tidepool-cli,io.tidepool:tidepool-report, in dependency order, without the parent and without the engine, which tidepool-cli needs. This shows what -pl does not do: it does not add the projects you forgot.

Reactor Build Order:

tidepool-model                                                     [jar]
tidepool-cli                                                       [jar]
tidepool-report                                                    [jar]

(The build was run with validate, which needs no compiled siblings. A phase that does need them fails when the dependency cannot be found; see Reactor resolution.) A selector that matches nothing is an error in Maven 3:

[ERROR] Could not find the selected project in the reactor: tidepool-nope

An exclusion is a ! before the selector. -pl '!tidepool-cli' builds everything except the CLI (the quotes keep the shell from interpreting the !):

Reactor Build Order:

tidepool                                                           [pom]
tidepool-model                                                     [jar]
tidepool-engine                                                    [jar]
tidepool-report                                                    [jar]

An exclusion of an unknown project is also an error on Maven 3.10.0 (Could not find the selected project in the reactor), and so is the ? prefix: -pl '?tidepool-nope,tidepool-model' failed with the same message on 3.9.16 and 3.10.0, with the ? printed as part of the name. Maven 4 documents the selector prefixes ! and - (exclude), + (include explicitly) and ? (optional: do not fail if the project does not exist). With Maven 4.0.0-rc-7, -pl '!:tidepool-cli,?:tidepool-nope' built the four remaining projects and printed:

[INFO] The requested optional projects :tidepool-nope do not exist.

(and the same line at the end of the build, in brackets). The -pl '+:tidepool-model,-:tidepool-cli' form built a single project.

A selector that names a parent behaves differently across major versions. mvn -pl :tidepool-parent validate built one project on Maven 3.10.0, the parent alone, whereas on Maven 4.0.0-rc-7 it built all five, because the Maven 4 guide says that selecting an aggregator automatically selects its subprojects (-N together with -pl switches that off). The habit that works on both is to name the modules and never rely on the parent.

-am: also make what the selection needs

mvn -pl tidepool-cli -am validate selects tidepool-cli and everything it needs. The needed projects are the engine, the model that the engine needs, and the parent that every module needs:

Reactor Build Order:

tidepool                                                           [pom]
tidepool-model                                                     [jar]
tidepool-engine                                                    [jar]
tidepool-cli                                                       [jar]

tidepool-report is absent: nothing in the CLI’s closure needs it. This is the command to run when you work on one module and want it to be built from sources against the current state of everything below it. It is the highlighted set of the dependency graph above.

-amd: also make what depends on the selection

-amd goes the other way. After a change to tidepool-model, the modules to rebuild are those that depend on it, directly or not. mvn -pl tidepool-model -amd validate:

Reactor Build Order:

tidepool-model                                                     [jar]
tidepool-engine                                                    [jar]
tidepool-cli                                                       [jar]
tidepool-report                                                    [jar]

The parent is absent, because the model does not depend on it in the sense of the sorting, only inherits from it. Combining the two, mvn -pl tidepool-engine -am -amd validate, builds the engine, what it needs, and what needs it:

Reactor Build Order:

tidepool                                                           [pom]
tidepool-model                                                     [jar]
tidepool-engine                                                    [jar]
tidepool-cli                                                       [jar]

tidepool-report still does not appear. It is neither needed by the engine nor does it depend on it; it only shares an ancestor. This is the usual mistake with -am -amd: it is not "everything related", it is the engine’s upstream and its downstream.

-rf and -N

-rf :tidepool-engine starts the build at that point in the sorted list and builds everything after it, including modules that do not depend on the engine:

Reactor Build Order:

tidepool-engine                                                    [jar]
tidepool-cli                                                       [jar]
tidepool-report                                                    [jar]

It does not rebuild what comes before, and that is where the trouble starts, see Resuming a failed build. -N builds only the project in the current directory: mvn -B -N validate in the tidepool directory printed a single project, the parent.

Reactor resolution

When the engine needs tidepool-model, where does Maven look for it? In a normal build, it looks in the local repository and then in the remote repositories. In a reactor build there is a step before: if the dependency is one of the projects in the reactor, Maven resolves it from that project’s build output. This is reactor resolution, and the part of Maven that does it is a workspace reader that knows what each reactor project has produced so far.

What it finds depends on how far the phases got. The two runs below used -X and kept the first three entries of the class path that the compiler plugin printed for tidepool-engine. After mvn compile -pl tidepool-model,tidepool-engine on Maven 3.10.0:

.../tidepool/tidepool-engine/target/classes
.../tidepool/tidepool-model/target/classes
.../m2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar

and after mvn package -DskipTests -pl tidepool-model,tidepool-engine:

.../tidepool/tidepool-engine/target/classes
.../tidepool/tidepool-model/target/tidepool-model-1.0.0.jar
.../m2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar

With compile the sibling is its target/classes directory, with package it is the JAR that the model module has just built. In neither case did Maven touch the local repository for tidepool-model. The same reactor then explains the three typical failures.

The sibling is not in the reactor. mvn -pl tidepool-engine compile, with nothing of tidepool installed, selects only the engine, so the model is looked for in the repositories, where it is not (Maven 3.10.0):

[ERROR] Failed to execute goal on project tidepool-engine: Could not resolve dependencies for project io.tidepool:tidepool-engine:jar:1.0.0
[ERROR] dependency: io.tidepool:tidepool-model:jar:1.0.0 (compile)
[ERROR] 	prefixes: Path io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar NOT allowed from central

The prefixes line is Maven 3.10 telling that the repository prefix file of Central says that no io/tidepool artifacts can be there, which ends the search at once (Maven 3.9.16 says Could not find artifact ... in central). The cure is -am: with mvn -pl tidepool-cli -am compile the four projects were built and the build ended with BUILD SUCCESS.

The sibling is installed, but stale. If tidepool-model was installed earlier and the build runs -pl tidepool-engine without -am, the class path points at the local repository instead of at the sources:

.../tidepool/tidepool-engine/target/classes
.../m2-stale/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
.../m2-stale/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar

Edits to the model are invisible to the engine in this build. Nothing warns about it; the build is green and wrong. The habit that avoids it is to name the dependencies in the build, with -am, or to install nothing between runs.

Installing one module is not enough. When the scratch repository got only the model, through mvn install -DskipTests -pl tidepool-model, the engine failed:

[ERROR] Failed to read artifact descriptor for io.tidepool:tidepool-model:jar:1.0.0
[ERROR] 	Caused by: The following artifacts could not be resolved: io.tidepool:tidepool-parent:pom:1.0.0 (absent): prefixes: Path io/tidepool/tidepool-parent/1.0.0/tidepool-parent-1.0.0.pom NOT allowed from central

The model’s POM names its parent, and a consumer needs the parent POM too (see the end of the section on relativePath in Inheritance, Aggregation & the Effective POM). mvn install -DskipTests -pl tidepool-model -am installed the parent as well and the engine built.

The recommendation of the Maven 4 What’s New page is a consequence of this section: do not use mvn clean install for regular builds, use mvn verify. A verify reactor build resolves siblings from the reactor and writes nothing outside the target directories, so it can neither read nor leave stale artifacts. The next one shows when you do need install.

Sharing test code with a test-jar

A cycle of the kind above is sometimes caused by test code: the engine’s tests want the fixtures of the model’s tests. The solution that the guide for attached tests describes is to package the model’s test classes as a second artifact and depend on it. The model module has Stations, a test fixture class in src/test/java, and the jar plugin’s test-jar goal (the plugin section shown earlier) packages the test classes into tidepool-model-1.0.0-tests.jar. The engine depends on it with <type>test-jar</type> and scope test; the classifier tests is implied by the type.

Before the configuration was added, the engine’s test did not compile, and the reactor showed the failure the way it does for any dependency problem (Maven 3.10.0; the log was trimmed to the lines about the missing class):

[ERROR] .../tidepool-engine/src/test/java/io/tidepool/engine/TideEngineTest.java:[5,25] cannot find symbol
  symbol:   class Stations
  location: package io.tidepool.model
...
tidepool-model ..................................... SUCCESS
[ERROR] tidepool-engine .................................... FAILURE

With the test-jar goal and the extra dependency, mvn clean test built all projects on Maven 3.9.16 and 3.10.0 and ran the two tests (StationTest in the model, TideEngineTest in the engine). The point is that test is enough: in the reactor, the test-jar was resolved from tidepool-model/target/test-classes: the test class path of the engine printed by -X listed tidepool-model/target/classes and tidepool-model/target/test-classes next to the engine’s own directories, as the main artifact was resolved from target/classes in the previous section.

Maven 4.0.0-rc-7 behaved differently on the very same build:

[ERROR] tidepool-engine .................................... FAILURE
[ERROR] org.apache.maven.api.services.ModelBuilderException: 1 problem was  for io.tidepool:tidepool-parent:pom:1.0.0
[ERROR]     - [FATAL] Non-resolvable parent POM: The following artifacts could not be resolved: org.example.corp:example-corporate-parent:pom:7 (absent): prefixes: Path org/example/corp/example-corporate-parent/7/example-corporate-parent-7.pom NOT allowed from central

(The text 1 problem was for is as printed by that release.) The company parent is not part of the tidepool reactor: it is found through relativePath only while Maven reads the parent POM from disk, and Maven 4.0.0-rc-7 needed it in a repository as soon as the engine’s dependencies were resolved from the model’s build output. With the company parent absent from the repository, mvn compile succeeded, whereas mvn test and mvn package failed with this error in the engine. After mvn install of the company parent into the scratch repository, mvn clean test on Maven 4.0.0-rc-7 ended with BUILD SUCCESS for all projects. I did not look into why the later phases need it; if your company parent is deployed, as it should be, you will not notice, and if you build a project whose parent lives only in a neighbouring directory, you will.

A test-jar publishes tests as an artifact, so it adds to the published surface of the module. Use it for fixtures shared within a build; shared fixtures that other teams depend on are better in a module of their own.

Failure policies

By default the reactor is fail fast: the first failed project stops the build, and the rest are skipped. To see the policies side by side, a scratch copy of tidepool got a test BrokenTest that always fails in the engine. The summaries are those of Maven 3.9.16 (build order) after mvn verify, mvn -fae verify and mvn -fn verify:

# mvn verify                          # mvn -fae verify                      # mvn -fn verify
tidepool ................. SUCCESS    tidepool ................. SUCCESS    tidepool ................. SUCCESS
tidepool-model ........... SUCCESS    tidepool-model ........... SUCCESS    tidepool-model ........... SUCCESS
tidepool-engine .......... FAILURE    tidepool-engine .......... FAILURE    tidepool-engine .......... FAILURE
tidepool-cli ............. SKIPPED    tidepool-cli ............. SKIPPED    tidepool-cli ............. SUCCESS
tidepool-report .......... SKIPPED    tidepool-report .......... SUCCESS    tidepool-report .......... SUCCESS
BUILD FAILURE                         BUILD FAILURE                         BUILD FAILURE

(The three summaries were put side by side by hand and the timings removed; each column is the output of one run.) The policies behave as follows:

  • Fail fast (-ff, the default): stops at the engine; the CLI and the report are skipped, although the report does not need the engine.

  • Fail at end (-fae): continues with every project that does not depend on a failed one. The CLI needs the engine, so it is skipped; the report only needs the model, so it is built. The exit code is still non-zero. This is the right choice for a CI server that wants to report all independent failures in one run.

  • Fail never (-fn): continues with all projects, even those that depend on the failed one. The CLI was built, and its compilation used whatever the engine had produced before its tests failed. The build result is still BUILD FAILURE, as the last line shows; -fn changes how the reactor proceeds, not the message, which makes it dangerous in scripts: look at the exit code, and know that downstream modules were built from a project that failed its tests.

Maven 3.10.0 and 4.0.0-rc-7 printed the same results but grouped them by outcome, with the skipped projects first and the failed project last. With -fae the 3.10.0 summary read tidepool-cli SKIPPED, then the three successes, then tidepool-engine FAILURE.

Resuming a failed build

After the failure, Maven 3.10.0 ended its output with a hint:

[ERROR] After correcting the problems, you can resume the build with the command
[ERROR]   mvn <args> -rf :tidepool-engine

The hint is correct in the case of the first module of a chain and wrong in the general case. After fixing the broken test (for the demonstration -Dtest='!BrokenTest' excluded it), mvn verify -rf :tidepool-engine started at the engine, as the [1/3] of the header shows, and failed immediately:

[INFO] Building tidepool-engine 1.0.0                                     [1/3]
[ERROR] Failed to execute goal on project tidepool-engine: Could not resolve dependencies for project io.tidepool:tidepool-engine:jar:1.0.0
[ERROR] dependency: io.tidepool:tidepool-model:jar:1.0.0 (compile)
[ERROR] dependency: io.tidepool:tidepool-model:jar:tests:1.0.0 (test)

The model was built in the first run, but it is not in the reactor of the second one, so reactor resolution cannot find it (see the previous section), and it is not in a repository either. Adding -am is the obvious attempt, and it does not work: the Maven 4 What’s New page names the cause, the long-standing bug MNG-6863, which made --also-make ignored together with --resume-from. I ran mvn verify -rf :tidepool-engine -am on Maven 3.9.16 and on 3.10.0, and both still built just the engine and failed with the same error. Two workarounds exist on Maven 3:

  • resume from an earlier project, here mvn verify -rf :tidepool-model, which rebuilt the model, the engine, the CLI and the report ([1/4] to [4/4]) and ended with BUILD SUCCESS. It rebuilds more than needed, but it is correct,

  • build from an installed state, by running mvn install before the failing step: slower, and it brings back the stale JAR problem of the previous section.

Maven 4 fixes this in two ways, and I ran both on 4.0.0-rc-7. mvn verify -rf :tidepool-engine -am now honours -am: it built the five projects (the parent, the model, the engine, the CLI and the report). And the new -r (--resume) does not need a project name. After the failed run, Maven 4 wrote target/resume.properties in the root directory:

#Fri Oct 16 15:06:17 CEST 2026
remainingProjects=io.tidepool\:tidepool-engine, io.tidepool\:tidepool-cli, io.tidepool\:tidepool-report

and its hint at the end of the failed build was mvn [args] -r. After the test was fixed, mvn -r verify printed:

[INFO] Resuming from io.tidepool:tidepool-engine, io.tidepool:tidepool-cli, io.tidepool:tidepool-report due to the --resume / -r feature.
[INFO] Building tidepool-engine 1.0.0                                     [3/5]
[INFO] Building tidepool-cli 1.0.0                                        [4/5]
[INFO] Building tidepool-report 1.0.0                                     [5/5]
[INFO] BUILD SUCCESS

The projects that had been built successfully (the parent and the model) were not built again, and the engine still found the model. The Maven 4 What’s New page says that the reactor is now aware of the projects that were built, and this is what it looks like. Maven 4.0.0-rc-7 is a release candidate, so the file name and the wording of these messages may change.

Parallel builds

-T builds independent modules at the same time. -T 4 uses four threads and -T 1C one thread per core (-T 2.5C is also legal). The reactor graph decides what can run in parallel: a project starts when every project it depends on has finished, and the other projects wait. In tidepool the parent comes first, the model second, and after that the engine and the report are independent, so they can overlap; the CLI waits for the engine.

On the 10-core scratch machine, mvn -T 1C verify on Maven 3.10.0 printed the marker line that tells you that the threaded builder is in use, and the Building lines show the new order of starts: the report started before the CLI, because it did not have to wait.

[INFO] Using the MultiThreadedBuilder implementation with a thread count of 10
[INFO] Building tidepool 1.0.0                                            [1/5]
[INFO] Building tidepool-model 1.0.0                                      [2/5]
[INFO] Building tidepool-engine 1.0.0                                     [3/5]
[INFO] Building tidepool-report 1.0.0                                     [4/5]
[INFO] Building tidepool-cli 1.0.0                                        [5/5]
...
[INFO] BUILD SUCCESS
[INFO] Total time:  4.293 s (Wall Clock)

The build of the same five projects without -T took 3.373 s. A parallel build of this size is not faster: the build is a chain (model, then engine, then CLI), and starting the threads and loading the plugins cost more than the independent report saves. Maven 4.0.0-rc-7 gave 3.178 s and 2.705 s for the two runs, with a different order of starts (the report started before the engine). The measure that matters is a project with many independent modules; the cache, -T and Maven Daemon are discussed on Build Performance: Cache, Parallel Builds & mvnd.

Two rules apply to parallel builds. First, the plugins must be thread-safe: a Mojo declares this with the @ThreadSafe annotation, and the wiki page on parallel builds shows a static lock around the Mojo code as the workaround for a plugin that keeps shared state. I did not run a build with a plugin that is not thread-safe, so I have no output for it. Second, the [n/m] positions are start positions: with -T, the lines of different modules can interleave in the log.

installAtEnd and deployAtEnd

By default each module is installed or deployed in its own install or deploy phase, as soon as that module is built. In a reactor with a failure this leaves half a release: the modules that were built before the failure are published, the others are not. The install and deploy plugins have a parameter for it: installAtEnd of the install plugin (since 2.5, marked experimental in its documentation) and deployAtEnd of the deploy plugin (since 2.8). The documentation of the install plugin says that if it is set and the build fails, none of the reactor projects is installed.

With the failing engine and -fae, mvn install on Maven 3.10.0 (install plugin 3.2.0) put these JARs into a scratch repository:

io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0-tests.jar
io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
io/tidepool/tidepool-report/1.0.0/tidepool-report-1.0.0.jar

The model and the report were installed, although the build failed: the report is a successful independent project. With -DinstallAtEnd=true the same build left the repository without any io/tidepool content. The deploy plugin behaved the same way for mvn deploy -fae -DaltDeploymentRepository=scratch::file:///.../repo, with the repository on a directory (nothing was sent to any server): Maven 3.9.16 (deploy plugin 3.1.4), 3.10.0 and 4.0.0-rc-7 (deploy plugin 3.2.0 in both) deployed the three JARs listed above by default, and none with -DdeployAtEnd=true.

That is a difference from what the Maven 4 What’s New page promises: Maven 4 changes the default of deployAtEnd to true, so that a deployment only happens when all subprojects were built successfully. The default is a property of the plugin, not of Maven. With the plugin versions that the Maven 4.0.0-rc-7 lifecycle bindings select (the Super POM binds no plugins), maven-install-plugin 3.2.0 and maven-deploy-plugin 3.2.0, it was still false, as the output above shows. I then pinned both plugins to 4.0.0-beta-3 (a pre-release of the plugins for Maven 4) in the parent’s pluginManagement. With them the failing build installed and deployed nothing by default, and -DinstallAtEnd=false restored the early installation of the model and the report. In the successful build, the five Installing lines for the JARs were all printed after the last module had been built.

Neither parameter makes the batch atomic. The documentation of the deploy plugin says explicitly that the deferred deployment is not a transaction: if a repository fails midway through the batch, the repositories already served keep their artifacts and nothing is rolled back. The full publishing flow, with release versions, is on Install, Deploy & Distribution Management, and the release plugin that wraps it on Releasing.

What Maven 4 changes

Everything below was run on Maven 4.0.0-rc-7, a release candidate; details may change before 4.0.0.

  • Subprojects. The model version 4.1.0 renames <modules> to <subprojects>; the old element still works. The setup, the parent inference and the consumer POM that goes with it are on Inheritance, Aggregation & the Effective POM and Maven 4 POM Model & Consumer POM.

  • --resume. -r resumes at the last failed project and remembers the projects that were built (see the resume section above).

  • --also-make with --resume-from works (MNG-6863 is fixed).

  • Optional and explicit selectors ?, + and - for -pl, and the selection of children with their aggregator.

  • Root discovery. Maven 4 finds the root of a multi-project build by walking up from the starting POM until it finds a POM with a sibling .mvn directory or a POM with root="true" in the 4.1.0 model, and uses that root to resolve siblings. Without either, it takes the starting POM as the root; the Maven 4 guide recommends creating the .mvn directory.

  • Subfolder builds. The What’s New page mentions that the reactor is aware of builds that start in a subfolder (MNG-6118). I ran the engine’s directory with .mvn in the root. Without -am, mvn verify in tidepool-engine failed like the Maven 3.10.0 run (Could not resolve dependencies, model not found). With mvn -am verify Maven 4.0.0-rc-7 loaded the whole root reactor and built only what the engine needs:

    [INFO] Building tidepool 1.0.0                                            [3/5]
    [INFO] Building tidepool-model 1.0.0                                      [4/5]
    [INFO] Building tidepool-engine 1.0.0                                     [5/5]
    ...
    tidepool ........................................... SUCCESS
    tidepool-model ..................................... SUCCESS
    tidepool-engine .................................... SUCCESS
    BUILD SUCCESS

    (The positions [3/5] to [5/5] count in the whole root reactor, not only in the three selected projects, which is what makes the logs of a subfolder build confusing.) Maven 3.10.0 with -am in the same directory built the engine alone and failed. A subfolder build is useful to run a tool (a web server, for example) only for one subproject.

  • Same timestamp for all subprojects. The What’s New page states that all subprojects now get the same SNAPSHOT timestamp in the local and remote repositories (MNG-6754). I did not run this; it needs a snapshot deployment to inspect.

  • The installation and deployment defaults follow the plugin versions, as the previous section shows. The migration notes are on Migrating to Maven 4.

The same idea in other tools

Need Maven Ant Gradle

Declare the set of projects

<modules> in an aggregator POM (<subprojects> in Maven 4)

a list of files given to subant, see Multi-File Builds: import, include & subant

include(...) in settings.gradle.kts, see Multi Project Builds

Order of the projects

computed by the reactor from the dependencies

the order of the subant list, written by hand; Ivy can sort by dependency, see Ivy: Publishing & Multi-Project Builds

computed from the task graph; a project dependency adds the needed task of the other project

Build one project and what it needs

-pl x -am

call the build file of that project and the ones it needs

gradle :x:build, the dependencies' tasks run as needed

Continue after a failure

-fae, -fn

failonerror="false" on subant, or the -k option of the ant command

--continue

Run in parallel

-T 4, -T 1C

parallel task, by hand

--parallel

The row to remember is the second: in Maven the order is computed and the flags select, in Ant it is written, and in Gradle it follows from the task dependencies. The migration view is in Migrating Between Tools.

Pitfalls

  • Order in <modules> is not the build order, except as a tie-breaker. Do not fix a failing build by reordering the list.

  • -pl without -am on a clean machine fails with Could not resolve dependencies; on a developer machine it builds against a stale installed JAR instead. Both are the same mistake.

  • -rf on Maven 3 does not rebuild what the failed module needs. Resume from the earliest module it needs, or use Maven 4 and -r.

  • -fn hides failures. The summary says BUILD FAILURE and the downstream modules were built anyway; prefer -fae.

  • install in a regular build leaves artifacts in the local repository that a later -pl build will use. Use verify unless a later step needs the installed artifact.

  • Parent POMs outside the reactor. A consumer, or Maven 4 resolving a test-jar, needs the whole parent chain in a repository.

  • -pl with a parent on Maven 3 and Maven 4 selects different sets of projects; name the modules.

  • Exclusion in a shell. Quote '!module', or the shell expands it as history on some shells.

Where the Maven 2 book is outdated

Apache Maven 2 Effective Implementation (Maven 2.2) builds subsets of a reactor with the Maven Reactor Plugin (reactor:resume, reactor:make, reactor:make-dependents, reactor:make-scm-changes) and presents the reactor options of the command line as recent. The plugin is retired: its site is marked "RETIRED", "no longer maintained", its last version is 1.1 (2014) and the plugin list describes it as "Maven 2 only". Its goals are built into the command line, as this page shows: -rf for reactor:resume, -pl x -am for reactor:make and -pl x -amd for reactor:make-dependents. Maven 4 adds -r and the <subprojects> element (see What Maven 4 changes); the command line has no counterpart of reactor:make-scm-changes.