Tired of sluggish Apache Maven builds slowing down your development cycle? Maven Reactor Modules allow you to split a project into smaller, independently buildable units. Discover how strategically breaking down your application into Maven Reactor Modules can dramatically accelerate build times through parallel execution and improve your overall software architecture.
- The Maven Build Bottleneck: Why So Slow?
- Introducing the Solution: Maven Reactor Modules
- "Feed the Daemon": How Modules Unlock Parallelism
- Beyond Speed: More Modules and the Architectural Advantages
- Getting Started: A Basic Multi-Module Structure
- Watch-Outs: Considerations and Trade-offs
- Conclusion: From 🐌 Snail’s Pace to 🚀 Rocket Speed
The Maven Build Bottleneck: Why So Slow?
Many Apache Maven reactor projects, especially those with multiple reactor modules, will greatly benefit from using the Apache Maven Daemon (mvnd
). This is due to the fact that often reactor modules of the Apache Maven build can be compiled in parallel. However, depending on the structure of the inter-module dependencies, the build could still end up mostly serial. This will result in slow build time (compilation, test execution, packaging).
This is especially true in single-module projects where no modules can be parallelized. Using the Apache Maven Daemon will not yield many benefits, except the already-running daemon (the Java VM executing Apache Maven).
.jar
project# main build file
pom.xml
# directory with java source files
src/main/java
# directory with tests
src/test/java
# created artifact though `mvn package`
target/myproject.jar
So, how can we ease the pain of slow builds, resulting from a serial reactor module execution order?
Introducing the Solution: Maven Reactor Modules
If you have never worked with Apache Maven reactor modules before, here is a small primer: Instead of having just one pom.xml
file producing a single .jar
or .war
file as its main result, a reactor has multiple pom.xml
files, arranged in a hierarchical directory structure. Each directory containing a subdirectory with a pom.xml
file will not create a .jar
artifact. Instead, it will point to one or multiple subfolders containing pom.xml
projects which will produce the .jar
artifacts.
However, most of those reactor modules will usually have a dependency on other modules of this so-called reactor build. Apache Maven will figure out in which order those modules can be built without creating a dependency cycle, then proceed.
Each module contains its own package, classes and tests. Those will be shielded from other classes unless a direct compile- or provided-scope dependency was declared.
# entry-point
.
|- pom.xml [packaging=pom]
|- module_1
| |- pom.xml
| |- src/main/java # source files of module_1
| `- target/module_1.jar # created artifact of module_1
`- module_2_cli_app
|- pom.xml
|- src/main/java # source files of module_2_cli_app
`- target/module_2_cli_app.jar # created artifact of 2nd module
Potentially, multiple modules of this reactor build can be built at the same time — in parallel. And this is where we are heading.
"Feed the Daemon": How Modules Unlock Parallelism
In the example above, we only have two modules. They need to be built sequentially, as the "cli app module" depends on "module_1".
To really get some gain, we need more modules. Maybe your application can be torn down into multiple "libraries".
If multiple modules do not have inter-project dependencies on each other, they can be built in parallel. The number of parallelism can be set using the -T
(threads) flag on mvn
and defaults to 1. The Apache Maven Daemon (mvnd
) defaults to the number of cores minus one (nproc -1
), with a minimum of 1.
For each of those independent reactor modules, compilation and test execution will be done at the same time. This will lead to better utilization of resources, like CPU and memory (RAM).
Beyond Speed: More Modules and the Architectural Advantages
Dividing the example into more modules
# entry-point
.
|- pom.xml [packaging=pom]
|- module_1
| |- pom.xml
| |- src/main/java # source files of module_1
| `- target/module_1.jar # created artifact of module_1
|- module_2
| |- pom.xml
| |- src/main/java # source files of module_2
| `- target/module_2.jar # created artifact of module_2
`- module_3_cli_app
|- pom.xml (declares dependency on modules 1 and 2)
|- src/main/java # source files of module_3_cli_app
`- target/module_3_cli_app.jar # created artifact of 3rd module
Given the extended example and a system with at least three CPU cores, modules module_1
and module_2
will be compiled, tested and packaged at the same time. The output of mvnd
will look like this:
Building app-modularized daemon: 58ed8128 threads used/hidden/max: 2/0/7 progress: 2/3 67% time: 00:00
:module_1 surefire:3.5.3:test (default-test)
:module_2 surefire:3.5.3:test (default-test)
Now two of your CPUs are busy working on two modules. However, this will still leave some idle cores around, and the very last module will always be built on its own.
Bottlenecks and creating more modules
Time intervals where only one module at a time is being built is called a bottleneck. However, 1/3rd (about 33%) of the built time was shaved off. Can we do better?
Yes:
Divide your application into specific services
Make each outgoing communication a single module
Group API collections into one or more modules, depending on dependencies
Create separate modules for each database connection (DAO or repository)
Isolate language additions (e.g., common-lang, stream-utils, concurrent-utils) into their own modules
Think in terms of ownership and responsibility: How much should a module with business logic know about third-party systems, encryption, or databases?
At the end of the build you will have one or more final artifacts. The more artifacts you produce, the smaller the bottleneck at the end of your build will be:
CLI modules
web applications
docker images
native images
etc.
Architectural Advantages and Benefits
Now with your application trimmed down into small modules, let’s explain the Architectural Benefits mentioned in the heading:
Imagine as part of your built, you have a services/api
module, services/github
and services/ldap
. If you want to test the GitHub service which in turn uses user information from an LDAP service, you now only have the LdapService.java
interface available in your tests.
You are now being forced to mock the LdapService
- something you would usually do with ArchUnit or similar software (ArchUnit, a tool for enforcing architectural rules, achieves similar isolation). But this "restriction" to not having a real implementation of an unrelated service available comes for free when chopping down your application into smaller pieces.
Getting Started: A Basic Multi-Module Structure
All demos in the next two sections are uploaded to this repository: bmarwell/snail2rocket-feed-the-daemon-demo. Please check it out and see if you get similar results!
This example will slow test execution by defining methods in src/main/java
with a Thread#sleep()
method with a default sleep period of 100ms. You can experiment with the sleep period using mvnd verify -Dmethod.timeout={timeout_ms}
.
The simple app
The simple app consists of five modules: bmarwell/snail2rocket-feed-the-daemon-demo/tree/main/app-simple. If you execute mvnd verify
, you should see something like this:
[INFO] Reactor Summary for App (simple) 1.0.0-SNAPSHOT:
[INFO]
[INFO] App (simple) ....................................... SUCCESS [ 0.003 s]
[INFO] App (simple) :: common ............................. SUCCESS [ 2.515 s]
[INFO] App (simple) :: services ........................... SUCCESS [ 9.370 s]
[INFO] App (simple) :: web ................................ SUCCESS [ 0.001 s]
[INFO] App (simple) :: web :: rest ........................ SUCCESS [ 2.004 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 14.046 s (Wall Clock)
[INFO] Finished at: 2025-04-24T20:13:56+02:00
[INFO] ------------------------------------------------------------------------
14 seconds is already a good time.
However, most of this app was executed sequentially, as there is only one service module containing classes like GitHubservice
, LdapService
, MailService
and others.
Refactored: More Modules for the same app
The second example in the repository contains the very same classes and tests! However, most service classes were dragged into their own Maven Reactor Module.
Let’s see what we get:
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for App (modularized) 1.0.0-SNAPSHOT:
[INFO]
[INFO] App (modularized) .................................. SUCCESS [ 0.000 s]
[INFO] App (modularized) :: common ........................ SUCCESS [ 0.000 s]
[INFO] App (modularized) :: common :: lang ................ SUCCESS [ 1.898 s]
[INFO] App (modularized) :: common :: value ............... SUCCESS [ 2.720 s]
[INFO] App (modularized) :: db ............................ SUCCESS [ 0.000 s]
[INFO] App (modularized) :: db :: api ..................... SUCCESS [ 0.027 s]
[INFO] App (modularized) :: db :: jpa ..................... SUCCESS [ 3.014 s]
[INFO] App (modularized) :: services ...................... SUCCESS [ 0.000 s]
[INFO] App (modularized) :: services :: api ............... SUCCESS [ 0.048 s]
[INFO] App (modularized) :: services :: commandhandler .... SUCCESS [ 4.146 s]
[INFO] App (modularized) :: services :: github ............ SUCCESS [ 2.685 s]
[INFO] App (modularized) :: services :: LDAP .............. SUCCESS [ 2.436 s]
[INFO] App (modularized) :: services :: mail .............. SUCCESS [ 2.606 s]
[INFO] App (modularized) :: services :: user .............. SUCCESS [ 2.697 s]
[INFO] App (modularized) :: services :: health ............ SUCCESS [ 2.688 s]
[INFO] App (modularized) :: web ........................... SUCCESS [ 0.000 s]
[INFO] App (modularized) :: web :: rest ................... SUCCESS [ 1.605 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.586 s (Wall Clock)
[INFO] Finished at: 2025-04-24T20:21:06+02:00
[INFO] ------------------------------------------------------------------------
Wow, that’s 8.5 instead of 14.3 seconds! We almost cut down the compilation-, test- and packaging time in half!
Why does this work? Because…
The Java compiler (
javac
) can be executed simultaneously for fewer input files.Idle time in tests will be used for other tests from other modules.
Packaging (
.jar
artifacts) will contain fewer filesPackaging can be done in parallel.
Watch-Outs: Considerations and Trade-offs
Now you may think: »Great! Let’s chop down all our projects into many, many modules!«
But, alas, there is always a catch (as always). Things to consider:
More modules mean greater project complexity. Your team will need to adapt to this setup.
Navigation between source files may become harder, depending on how used you are to navigation within your IDE.
Inter-Reactor dependencies can be complicated: you will likely make use of the
provided
-scope more often than you did before.More "ceremony" (additional setup overhead) for your projects: Adding features will now often lead to more modules. While this benefits compilation and general build time, there is a small new overhead in creating yet another module and adding it as a runtime dependency to your final jar/war.
Now, there is more:
If you are developing a library, your users will need to have a more complex setup as a consumer.
Each change to your library will be potentially a breaking change for users.
For small projects with minimal build times, the complexity of multi-module setups may outweigh the benefits.
Conclusion: From 🐌 Snail’s Pace to 🚀 Rocket Speed
In this article, we learned how to significantly speed up your build by modifying the architecture of your software. In some cases, you can cut down total build time not only by half, but even by a factor of five or more. Plus you get enforced architectural discipline at no extra cost.
So, should you use this technique? As always: it depends. Do experiment in your projects and see if you seed a benefit in either architecture (clearer responsibilities, less complex testing) or build time - or both. If so, show this to your team and speak about adapting a more complex reactor build!
This article is part of the #SnailToRocket series.