Maven Performance, Parallel Builds, Daemon Options, Reproducible Builds, and Troubleshooting: Guided Hands-On Workflow and Core Operations
Measure cold, warm, serial, and parallel Maven builds in a disposable reactor, inspect plugin thread safety, and compare reproducible artifacts with controlled evidence.
This workflow creates a disposable four-module reactor with one shared prerequisite and two independent branches. That graph gives Maven real parallel work while keeping the lab small enough to inspect file by file.
Learning objectives
- Create a disposable wrapper-driven reactor with an observable common → {alpha,beta} → app graph.
- Capture serial/cold, serial/warm, and parallel/warm timings without confusing repository warmth with parallelism.
- Inspect plugin/build-plan evidence before using -T.
- Compare artifacts across two isolated workspaces and separate local repositories.
- Use -e and -X only as controlled diagnostic escalations.
- Choose a performance control from evidence rather than habit.
1. Start from a trusted wrapper and isolate every lab-owned state store
Do not download ad-hoc wrapper scripts into a new directory. Start beside a project whose Maven Wrapper files were already generated and verified in Chapter 4, then copy that trusted wrapper into the disposable lab.
set -euo pipefail
LAB="$PWD/../maven-performance-lab"
mkdir -p "$LAB"/{evidence,.lab}
cp mvnw mvnw.cmd "$LAB/"
cp -R .mvn "$LAB/"
cd "$LAB"
./mvnw -v | tee evidence/maven-version.txt
java -version 2> evidence/java-version.txt
printf '%s
' 'Prediction A: cold resolution is slower than warm resolution on the same machine.' 'Prediction B: alpha and beta may overlap after common with -T 2.' 'Prediction C: fixed outputTimestamp should keep JAR bytes stable across equivalent builds.' > evidence/predictions.txt
.\mvnw.cmd and
project-relative paths such as $PWD\.lab\repo-cold. Use
Measure-Command { .\mvnw.cmd ... } for timing. Keep all
temporary repositories under this lab directory.
2. Author the reactor and make the graph explicit
<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>dev.academy</groupId>
<artifactId>perf-reactor</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>common</module>
<module>alpha</module>
<module>beta</module>
<module>app</module>
</modules>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.outputTimestamp>2026-08-24T00:00:00Z</project.build.outputTimestamp>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.academy</groupId><artifactId>common</artifactId><version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.academy</groupId><artifactId>alpha</artifactId><version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.academy</groupId><artifactId>beta</artifactId><version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-clean-plugin</artifactId><version>3.5.0</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>3.5.0</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.15.0</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.5.1</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-install-plugin</artifactId><version>3.1.4</version></plugin>
</plugins>
</pluginManagement>
</build>
</project>
<!-- common/pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>dev.academy</groupId><artifactId>perf-reactor</artifactId><version>1.0.0</version><relativePath>../pom.xml</relativePath></parent>
<artifactId>common</artifactId>
</project>
<!-- alpha/pom.xml and beta/pom.xml: change artifactId accordingly -->
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>dev.academy</groupId><artifactId>perf-reactor</artifactId><version>1.0.0</version><relativePath>../pom.xml</relativePath></parent>
<artifactId>alpha</artifactId>
<dependencies><dependency><groupId>dev.academy</groupId><artifactId>common</artifactId></dependency></dependencies>
</project>
<!-- app/pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>dev.academy</groupId><artifactId>perf-reactor</artifactId><version>1.0.0</version><relativePath>../pom.xml</relativePath></parent>
<artifactId>app</artifactId>
<dependencies>
<dependency><groupId>dev.academy</groupId><artifactId>alpha</artifactId></dependency>
<dependency><groupId>dev.academy</groupId><artifactId>beta</artifactId></dependency>
</dependencies>
</project>
The root POM is both parent and aggregator. The dependency graph is
not the same thing as module declaration order:
alpha and beta each depend on
common; app depends on both branches.
Maven's reactor can therefore overlap alpha and beta but cannot
legally build app before either branch.
3. Add tiny deterministic Java sources
mkdir -p common/src/main/java/dev/academy/common
mkdir -p alpha/src/main/java/dev/academy/alpha
mkdir -p beta/src/main/java/dev/academy/beta
mkdir -p app/src/main/java/dev/academy/app
cat > common/src/main/java/dev/academy/common/Token.java <<'EOF'
package dev.academy.common;
public final class Token {
private Token() {}
public static String value() { return "stable"; }
}
EOF
cat > alpha/src/main/java/dev/academy/alpha/Alpha.java <<'EOF'
package dev.academy.alpha;
import dev.academy.common.Token;
public final class Alpha { public static String value() { return "A-" + Token.value(); } }
EOF
cat > beta/src/main/java/dev/academy/beta/Beta.java <<'EOF'
package dev.academy.beta;
import dev.academy.common.Token;
public final class Beta { public static String value() { return "B-" + Token.value(); } }
EOF
cat > app/src/main/java/dev/academy/app/App.java <<'EOF'
package dev.academy.app;
import dev.academy.alpha.Alpha;
import dev.academy.beta.Beta;
public final class App { public static String value() { return Alpha.value() + ":" + Beta.value(); } }
EOF
These files are intentionally boring. Performance experiments are easier to reason about when application logic is not another changing variable.
4. Prove the model and execution plan before timing
./mvnw help:effective-pom -Doutput=evidence/effective-pom.xml
./mvnw -DskipTests dependency:tree | tee evidence/dependency-tree.txt
./mvnw org.apache.maven.plugins:maven-artifact-plugin:3.6.1:check-buildplan -Dcheck.buildplan.tasks=verify | tee evidence/repro-buildplan.txt
Inspect the reactor order in Maven output and confirm the effective plugin versions. The Artifact Plugin goal is itself thread-safe and checks known reproducible-build issues in the requested plan. It does not certify all of your generated content; it is one piece of evidence.
5. Measure cold and warm repository cost separately
Use the same project, same thread count, same JDK, and same goal. Change only the local-repository state. The first command uses an empty lab repository; the second uses the now-warm same repository.
TIMEFORMAT='elapsed=%3R s'
COLD_REPO="$PWD/.lab/repo-cold"
mkdir -p "$COLD_REPO"
time ./mvnw -Dmaven.repo.local="$COLD_REPO" -T 1 clean verify | tee evidence/serial-cold.log
time ./mvnw -Dmaven.repo.local="$COLD_REPO" -T 1 clean verify | tee evidence/serial-warm.log
Interpretation: both runs execute a clean project build, but only the first must populate the isolated local repository. If the first run is slower, that difference is primarily resolver/download/setup cost—not proof that compilation became faster.
6. Enable reactor parallelism only after checking safety
The current Maven CLI accepts -T 2 for two build
threads or a multiplier such as -T 1C. Start with a
small explicit value so the experiment is understandable.
time ./mvnw -Dmaven.repo.local="$COLD_REPO" -T 2 clean verify | tee evidence/parallel-warm.log
grep -E 'Building |Reactor Summary|WARNING|thread' evidence/parallel-warm.log || true
You are looking for two things: whether alpha/beta can overlap and whether Maven warns about goals not marked thread-safe. A successful exit status does not erase a thread-safety warning or a race in project-owned shared files.
7. Compare artifact identity after the performance change
sha256sum common/target/common-1.0.0.jar alpha/target/alpha-1.0.0.jar beta/target/beta-1.0.0.jar app/target/app-1.0.0.jar | tee evidence/parallel-sha256.txt
jar tf app/target/app-1.0.0.jar | tee evidence/app-jar.txt
The checksum set is the artifact-level invariant for this lab. A timing improvement is not accepted if expected files disappear, module scope changes, or equivalent serial/parallel builds produce unexplained byte differences.
8. Stronger check: build the same source in a second workspace and repository
Copy the source tree without generated target directories or evidence to a second lab directory. Use a separate local repository. This removes workspace and local-repository reuse from the artifact comparison.
cd ..
cp -R maven-performance-lab maven-performance-lab-b
find maven-performance-lab-b -type d -name target -prune -exec rm -rf {} +
rm -rf maven-performance-lab-b/evidence maven-performance-lab-b/.lab
mkdir -p maven-performance-lab-b/{evidence,.lab/repo}
cd maven-performance-lab-b
./mvnw -Dmaven.repo.local="$PWD/.lab/repo" -T 1 clean verify | tee evidence/isolated-build.log
sha256sum */target/*.jar | tee evidence/isolated-sha256.txt
find ... -exec, remove only each lab module's
target/ directory explicitly.
9. Use -e and -X only when a preserved failure needs more context
# First preserve the normal failure.
./mvnw -Dmaven.repo.local="$PWD/.lab/repo" verify > evidence/failure.log 2>&1 || true
# If the normal log lacks an exception cause chain:
./mvnw -e -Dmaven.repo.local="$PWD/.lab/repo" verify > evidence/failure-e.log 2>&1 || true
# Only if still necessary; treat this file as potentially sensitive:
./mvnw -X -Dmaven.repo.local="$PWD/.lab/repo" verify > evidence/failure-x.log 2>&1 || true
Do not time the -X invocation as though it were the
normal build. Debug logging itself changes I/O volume and may
include repository URLs, environment-derived configuration, and
other operational details. Review and redact before sharing.
10. Challenge: choose the control from the model
Your team says “CI is slow because Maven is slow.” Evidence shows
the first build after cache eviction is 70 seconds, the second is 24
seconds, and -T 2 reduces the warm build to 18 seconds
without warnings or checksum changes. Which claim is justified?
Expected reasoning: resolver/cache warmth explains
a larger portion of the cold penalty; bounded reactor parallelism
improves the warm path further. Preserve both conclusions
separately. Do not describe -T 2 as a 52-second
improvement because that mixes cache and concurrency variables.
11. Cleanup only disposable state
cd ..
rm -rf maven-performance-lab maven-performance-lab-b
Run this only if those exact directories are the disposable labs you created above. Do not substitute a user home, shared CI cache, or existing project path.
Knowledge check
Why do the serial-cold and serial-warm commands both use -T 1?
Holding thread count constant isolates local-repository warmth as the principal changed variable.
Why run check-buildplan before -T 2?
It helps inspect the selected execution plan and reproducibility issues; thread safety still requires checking active goal documentation and project-owned shared state.
If parallel build is faster but one module JAR checksum changes, is the optimization accepted?
No. The unexplained output difference is a correctness/reproducibility regression until diagnosed.
Why is a second workspace stronger than two builds in the same workspace?
It reduces the chance that undeclared workspace state or previous outputs make the rebuild look reproducible.
What does -X change besides observability?
It substantially increases log volume and I/O and may expose operational details, so its timing is not directly comparable to normal-mode timing.
12. Bridge to configuration tradeoffs
You now have separate measurements for resolver warmth, reactor concurrency, and artifact identity. Lesson 3 decides when those mechanisms belong in developer workflows, CI, or release verification—and when process isolation is more valuable than speed.
Official references and version notes
Version-sensitive statements in this lesson were checked against current Apache Maven primary documentation on 2026-08-24.
- Apache Maven download/current releases — Maven 3.9.16 is recommended; Maven 4 and mvnd 2.x are preview lines; mvnd 1.0.6 is current.
- Maven 3.9.16 release notes — Current Maven 3 behavior and release-specific changes.
- Configuring reproducible builds — project.build.outputTimestamp, build-plan checks, and independent rebuild guidance.
- Maven Daemon — Separate daemon infrastructure and mvnd invocation model.
- Maven multiple-modules guide — Reactor collection, sorting, and selected project behavior.
- Maven Artifact Plugin — Build-plan and reproducibility comparison tools.
- Maven AntRun Plugin run goal — Current example of goal documentation explicitly stating thread-safe/parallel-build support.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.