Maven 4 Preview, Build Consumer POM, Compatibility, Migration Planning, and Maven 3 Production Baselines: Guided Hands-On Workflow and Core Operations
Run one Maven 3 project through isolated Maven 3.9.16 and Maven 4.0.0-rc-6 lanes, compare evidence, inspect mvnup findings, and observe consumer POM behavior safely.
A compatibility test is useful only when you know exactly what changed. This workflow starts from one Maven 3-compatible project, creates separate Maven 3 and Maven 4 lanes, pins both runtimes, isolates their local repositories, and preserves every diff and checksum before touching model 4.1.0.
Learning objectives
- Create identical Maven 3 and Maven 4 test lanes without editing the production baseline in place.
- Verify and pin the Maven 4 RC distribution before executing it.
- Compare effective model, build/test result, JAR contents, and SHA-256 across both lanes.
- Run mvnup in check mode before applying any Maven 4-specific POM edit.
- Create an optional model 4.1.0 copy and inspect installed consumer metadata.
- Leave the original Maven 3 project immediately recoverable.
1. Start from a trusted Maven 3 wrapper and create disposable lanes
Use the wrapper established earlier in the course. Do not fetch wrapper scripts from a gist or paste unverified launchers into a repository. The baseline copy remains Maven 3.9.16; the candidate copy receives a separately verified Maven 4 distribution.
set -euo pipefail
ROOT="$PWD/../maven4-migration-lab"
rm -rf "$ROOT"
mkdir -p "$ROOT"/{baseline-m3,candidate-m4,evidence,downloads,.lab}
# Run from a trusted project already containing mvnw, mvnw.cmd, and .mvn/
cp mvnw mvnw.cmd "$ROOT/baseline-m3/"
cp -R .mvn "$ROOT/baseline-m3/"
cp mvnw mvnw.cmd "$ROOT/candidate-m4/"
cp -R .mvn "$ROOT/candidate-m4/"
cd "$ROOT/baseline-m3"
export MAVEN_USER_HOME="$ROOT/.lab/home-m3"
./mvnw -v | tee ../evidence/maven3-version.txt
java -version 2> ../evidence/java-baseline.txt
2. Put the exact same Maven 3-compatible project in both lanes
<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>maven4-migration-lab</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-08-24T00:00:00Z</project.build.outputTimestamp>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<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>
<configuration><release>17</release></configuration>
</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>
</plugins>
</build>
</project>
package dev.academy;
public final class App {
private App() {}
public static String identity() {
return "maven4-migration-lab";
}
}
package dev.academy;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class AppTest {
@Test
void identityIsStable() {
assertEquals("maven4-migration-lab", App.identity());
}
}
Create pom.xml,
src/main/java/dev/academy/App.java, and
src/test/java/dev/academy/AppTest.java in
baseline-m3, then copy those three repository-owned
inputs byte-for-byte into candidate-m4. The test gives
both lanes an independently visible Surefire result instead of
treating a zero-test build as verification.
cd "$ROOT"
mkdir -p baseline-m3/src/{main,test}/java/dev/academy
# Save the POM and the two Java files shown above at their stated paths.
cp baseline-m3/pom.xml candidate-m4/pom.xml
mkdir -p candidate-m4/src/{main,test}/java/dev/academy
cp baseline-m3/src/main/java/dev/academy/App.java candidate-m4/src/main/java/dev/academy/App.java
cp baseline-m3/src/test/java/dev/academy/AppTest.java candidate-m4/src/test/java/dev/academy/AppTest.java
sha256sum baseline-m3/pom.xml candidate-m4/pom.xml > evidence/source-pom.sha256
The two POM hashes must match before changing the Maven 4 wrapper. Model version 4.0.0 deliberately keeps Maven 3 compatibility while Maven 4 is being evaluated.
3. Verify Maven 4 RC before changing the candidate wrapper
Download the RC archive and its Apache-published SHA-512 sidecar. Verify SHA-512 first, then calculate the SHA-256 value required by the Maven Wrapper integrity property. This makes the candidate runtime both version-pinned and integrity-pinned.
cd "$ROOT/downloads"
M4_VERSION='4.0.0-rc-6'
M4_URL="https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/${M4_VERSION}/apache-maven-${M4_VERSION}-bin.zip"
curl -fL -o "apache-maven-${M4_VERSION}-bin.zip" "$M4_URL"
curl -fL -o "apache-maven-${M4_VERSION}-bin.zip.sha512" "$M4_URL.sha512"
EXPECTED_SHA512="$(tr -d '[:space:]' < "apache-maven-${M4_VERSION}-bin.zip.sha512")"
ACTUAL_SHA512="$(sha512sum "apache-maven-${M4_VERSION}-bin.zip" | awk '{print $1}')"
test "$EXPECTED_SHA512" = "$ACTUAL_SHA512"
M4_SHA256="$(sha256sum "apache-maven-${M4_VERSION}-bin.zip" | awk '{print $1}')"
printf '%s
' "$M4_SHA256" > ../evidence/maven4-distribution.sha256
Invoke-WebRequest for the ZIP and
.sha512 sidecar,
Get-FileHash -Algorithm SHA512 for Apache integrity
verification, and Get-FileHash -Algorithm SHA256 for the
wrapper pin. Use .\mvnw.cmd and the extracted
bin\mvnup.cmd in the equivalent Windows lane.
4. Pin only the candidate wrapper to Maven 4.0.0-rc-6
from pathlib import Path
p = Path("candidate-m4/.mvn/wrapper/maven-wrapper.properties")
text = p.read_text(encoding="utf-8")
lines = []
for line in text.splitlines():
if line.startswith("distributionUrl="):
lines.append("distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-rc-6/apache-maven-4.0.0-rc-6-bin.zip")
elif line.startswith("distributionSha256Sum="):
continue
else:
lines.append(line)
sha = Path("evidence/maven4-distribution.sha256").read_text().strip()
lines.append(f"distributionSha256Sum={sha}")
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
Run that script from $ROOT. The Maven 3 baseline
wrapper stays untouched. The candidate wrapper now names exactly one
Maven 4 RC distribution and its SHA-256 digest.
cd "$ROOT"
MAVEN_USER_HOME="$ROOT/.lab/home-m4" ./candidate-m4/mvnw -v | tee evidence/maven4-version.txt
Expected identity: Maven 4.0.0-rc-6 running on Java 21. If Java is below 17, stop before interpreting any project result.
5. Build the same model in isolated repositories
cd "$ROOT/baseline-m3"
MAVEN_USER_HOME="$ROOT/.lab/home-m3" ./mvnw -Dmaven.repo.local="$ROOT/.lab/repo-m3" clean verify | tee "$ROOT/evidence/maven3-verify.log"
MAVEN_USER_HOME="$ROOT/.lab/home-m3" ./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom -Doutput="$ROOT/evidence/effective-m3.xml"
sha256sum target/maven4-migration-lab-1.0.0.jar > "$ROOT/evidence/artifact-m3.sha256"
jar tf target/maven4-migration-lab-1.0.0.jar > "$ROOT/evidence/jar-m3.txt"
grep -R 'tests="1"' target/surefire-reports/TEST-*.xml > "$ROOT/evidence/tests-m3.txt"
cd "$ROOT/candidate-m4"
MAVEN_USER_HOME="$ROOT/.lab/home-m4" ./mvnw -Dmaven.repo.local="$ROOT/.lab/repo-m4" clean verify | tee "$ROOT/evidence/maven4-verify.log"
MAVEN_USER_HOME="$ROOT/.lab/home-m4" ./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom -Doutput="$ROOT/evidence/effective-m4.xml"
sha256sum target/maven4-migration-lab-1.0.0.jar > "$ROOT/evidence/artifact-m4.sha256"
jar tf target/maven4-migration-lab-1.0.0.jar > "$ROOT/evidence/jar-m4.txt"
grep -R 'tests="1"' target/surefire-reports/TEST-*.xml > "$ROOT/evidence/tests-m4.txt"
Compare the digest values and JAR member lists. Matching checksums are strong evidence for this simple controlled artifact, but still do not prove every plugin, profile, release path, or extension in a real repository is compatible.
6. Run the Maven Upgrade Tool read-only first
The wrapper launches Maven itself, but mvnup is a
separate executable shipped inside the Maven 4 distribution. Reuse
the archive you already verified, extract it only under the
disposable lab, and run its bin/mvnup.
cd "$ROOT"
mkdir -p .lab/tools
unzip -q downloads/apache-maven-4.0.0-rc-6-bin.zip -d .lab/tools
M4_HOME="$ROOT/.lab/tools/apache-maven-4.0.0-rc-6"
"$M4_HOME/bin/mvnup" check --directory candidate-m4 | tee evidence/mvnup-check-400.txt
"$M4_HOME/bin/mvnup" check --model-version 4.1.0 --all --directory candidate-m4 | tee evidence/mvnup-check-410.txt
The first check targets model 4.0.0 by default and focuses on Maven 4 compatibility improvements that can remain Maven 3-buildable. The second asks what would change if the project deliberately crosses the Maven 4-only model boundary.
7. Inspect model 4.1.0 and consumer POM behavior only in a second disposable copy
Do not overwrite the baseline or your first candidate. Create
candidate-41, then either let
mvnup apply --model-version 4.1.0 --all edit it or use
the compact 4.1.0 POM below for a focused consumer-POM
demonstration.
<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">
<modelVersion>4.1.0</modelVersion>
<groupId>dev.academy</groupId>
<artifactId>maven4-consumer-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-08-24T00:00:00Z</project.build.outputTimestamp>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<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>
<configuration><release>17</release></configuration>
</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>
</plugins>
</build>
</project>
cd "$ROOT"
cp -R candidate-m4 candidate-41
cp candidate-41/pom.xml evidence/pom-before-41.xml
# Option A: inspect what mvnup would apply, then apply in this disposable copy.
"$M4_HOME/bin/mvnup" apply --model-version 4.1.0 --all --directory candidate-41 | tee evidence/mvnup-apply-410.txt
# If you prefer the focused example, replace candidate-41/pom.xml with the
# model 4.1.0 POM shown above after saving the mvnup diff.
diff -u evidence/pom-before-41.xml candidate-41/pom.xml > evidence/pom-41.diff || true
cd candidate-41
MAVEN_USER_HOME="$ROOT/.lab/home-41" ./mvnw -Dmaven.repo.local="$ROOT/.lab/repo-41" clean install | tee "$ROOT/evidence/maven4-41-install.log"
INSTALLED_POM="$ROOT/.lab/repo-41/dev/academy/maven4-migration-lab/1.0.0/maven4-migration-lab-1.0.0.pom"
# If the focused POM uses artifactId maven4-consumer-demo, adjust this path accordingly.
test -f "$INSTALLED_POM" && cp "$INSTALLED_POM" "$ROOT/evidence/consumer-installed.pom" || true
For an ordinary Maven 4 JAR project using model 4.1.0, expect downstream-oriented installed/deployed metadata to differ from the source build POM. Inspect the actual file; do not hard-code an assumption about every element. Confirm coordinates and dependencies, note whether build-only plugin/properties content was removed, and note the model version Maven emitted for consumers.
8. Compare warnings and model differences, not only exit codes
cd "$ROOT"
grep -E '\[WARNING\]|\[ERROR\]' evidence/maven3-verify.log > evidence/warnings-m3.txt || true
grep -E '\[WARNING\]|\[ERROR\]' evidence/maven4-verify.log > evidence/warnings-m4.txt || true
diff -u evidence/warnings-m3.txt evidence/warnings-m4.txt > evidence/warning-diff.txt || true
diff -u evidence/effective-m3.xml evidence/effective-m4.xml > evidence/effective-diff.txt || true
A Maven 4 warning may represent a future build failure or a semantic difference worth fixing even when the build succeeds today. Preserve the exact warning and map it to a declared model or plugin before editing.
9. Challenge: choose the smallest safe next action
Your Maven 3 and Maven 4 builds both pass, but
mvnup check --model-version 4.1.0 proposes many
inference edits and your production release policy still requires
Maven 3 rollback. What should you do?
Answer before revealing: keep the source POM at model 4.0.0, continue the dual-baseline Maven 4 test lane, fix only Maven 4 compatibility issues that remain Maven 3-compatible, and defer 4.1.0-only edits until the rollback requirement is intentionally removed.
10. Cleanup and rollback
The Maven 3 baseline directory was never modified to Maven 4, so rollback is structural: delete the candidate directories and their isolated repositories. Do not delete your normal Maven local repository or wrapper distribution cache.
cd "$ROOT/.."
rm -rf maven4-migration-lab
Knowledge check
Why manually verify the Maven 4 ZIP if the wrapper also supports distributionSha256Sum?
Because you need the expected SHA-256 value from a trusted verification path before pinning it. The lab validates Apache SHA-512 first, then derives the SHA-256 used by the wrapper.
Why use separate repo-m3 and repo-m4 directories?
To prevent one lane from silently depending on locally installed or partially resolved state created by the other lane, making resolver differences easier to explain.
What does mvnup check change?
Nothing in the project; it analyzes and reports. Apply mode changes POM files and belongs only in a disposable copy/branch after review.
If the source 4.1.0 POM and installed consumer POM differ, is that automatically a bug?
No. Maven 4 intentionally separates build and consumer metadata. The question is whether the consumer metadata preserves the downstream contract.
Why is the first Maven 4 test performed with model 4.0.0?
It keeps Maven 3 compatibility and isolates Maven-core compatibility before introducing Maven 4-only model semantics.
11. Bridge to migration decisions
You now have the raw evidence needed for a policy decision. Lesson 3 converts that evidence into choices about Maven 3 support windows, dual-baseline CI, model 4.1.0 adoption, plugin/extension risk, and the cost of carrying two build-tool baselines.
Official references and version notes
Version-sensitive statements in this lesson were checked against current Apache Maven primary documentation on 2026-08-24.
- Maven releases history — Current GA and Maven 4 release-candidate status, release dates, and Java requirements.
- What is new in Maven 4 — Java 17 runtime requirement, consumer POM, model 4.1.0 features, BOM packaging, and Maven 4 behavior.
- Starting with Maven 4 — Official prepare → test → migrate strategy, compatibility changes, model 4.1.0 guidance, and rollback-friendly staging.
- Maven Upgrade Tool (mvnup) — Built-in Maven 4 migration tool, check/apply workflow, and 4.0.0 versus 4.1.0 target behavior.
- Maven 4.0.0-rc-6 release notes — Current RC status, migration notes, fixed RC-5 issues, and remaining known issues.
- Apache Maven Wrapper 3.3.4 — Stable wrapper release and integrity-capable wrapper behavior.
- Maven Wrapper checksum parameter — distributionSha256Sum supports integrity pinning of the Maven distribution.
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.