Chapter 13Lesson 02~185 minutes

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.

Measurement-TCold vs WarmChecksumsArtifact Plugin

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.

Current baseline — verified 2026-08-24. Mandatory labs use Apache Maven 3.9.16 through Maven Wrapper 3.3.4, JDK 21 as the build runtime, and Java 17 as the compiler release target. Maven Daemon 1.0.6 is an optional separate tool; Maven Daemon 2.0.0-rc-3 and Maven 4.0.0-rc-6 are preview releases and are not the production baseline. The project pins Clean 3.5.0, Resources 3.5.0, Compiler 3.15.0, Surefire 3.5.6, JAR 3.5.1, and Artifact Plugin 3.6.1.

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
Windows PowerShell: use .\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
Cleanup scope: the commands above delete only directories under the copied disposable lab. They do not touch the normal Maven user repository. If your shell does not support 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?

Why run check-buildplan before -T 2?

If parallel build is faster but one module JAR checksum changes, is the optimization accepted?

Why is a second workspace stronger than two builds in the same workspace?

What does -X change besides observability?

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.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.