Checkpoint Lab — Maven vs Gradle: Model Differences, Migration Strategies, Mixed Estates, and Tool Selection
Execute a bounded migration plan for a representative library, define invariants before changing build files, compare Maven and Gradle evidence, inject and repair one dependency mismatch, document non-equivalence honestly, and produce a rollback-safe tool recommendation.
Learning objectives
- Write migration invariants before changing build configuration.
- Execute side-by-side Maven/Gradle builds and compare compile, tests, dependency graph, artifact payload, metadata, and CI entry point.
- Inject and diagnose a dependency mismatch using each tool's native evidence.
- Document raw-byte and metadata non-equivalence instead of forcing false equality.
- Produce an explicit rollback path and a justified Maven/Gradle/mixed-estate recommendation.
Checkpoint rule. The existing Maven build remains authoritative until the Gradle candidate passes the defined invariants. Do not publish the candidate under a real production coordinate or modify shared repositories.
1. Scenario and acceptance contract
You maintain a small Maven library consumed by JVM services. The team is considering Gradle because other repositories already use tested Gradle conventions and caches. Your assignment is not “convert the POM.” It is to determine whether this library can migrate without changing its delivery contract.
| Invariant | Required proof |
|---|---|
| Source/API | Same reviewed source and public classes/methods. |
| Toolchain | Maven 3.9.16 / Gradle 9.7.1 wrappers recorded; JDK 21 compiler environment; Java 17 target. |
| Dependencies | Commons Lang resolves to 3.20.0 in both; consumer exposure differences are understood. |
| Tests | Exactly two fixture tests run and fail the build on failure. |
| Artifact | Required class/resource payload present; raw SHA-256 recorded; any byte difference explained. |
| Metadata | Same GAV; Maven-consumer scopes are intentional; Gradle-only metadata does not become an unreviewed requirement. |
| CI entry |
Maven clean verify and Gradle
clean build produce the required evidence.
|
| Publication | Only the authoritative pipeline may publish the immutable release coordinate. |
2. Preflight and exact assumptions
Use a disposable ch30-checkpoint directory.
Preconditions:
- Maven Wrapper 3.3.4 pinned to Maven 3.9.16;
- Gradle Wrapper pinned to Gradle 9.7.1;
- JDK 21 available to run both tools and compile;
- Java
--release 17target in both build models; - Maven Compiler 3.15.0, Surefire 3.5.6, JAR 3.5.1;
- Commons Lang 3.20.0 and JUnit 6.1.3;
- lab-local Maven repository and Gradle User Home;
- no production credentials, repository writes, or global cache deletion.
If dependencies/wrapper distributions are unavailable because the lab is offline and not already cached, record the blocked cells rather than weakening version/security policy.
3. Create the authoritative Maven baseline and Gradle candidate
Create the same source/test fixture from Lesson 2 in both directories. Keep the build descriptors native:
<?xml version="1.0" encoding="UTF-8"?>
<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.migration</groupId>
<artifactId>greeting-lib</artifactId>
<version>1.0.0</version>
<name>Greeting Library</name>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-01-01T00:00:00Z</project.build.outputTimestamp>
<commons-lang3.version>3.20.0</commons-lang3.version>
<junit.version>6.1.3</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<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>
</plugins>
</build>
</project>
rootProject.name = "greeting-lib"
plugins {
`java-library`
`maven-publish`
}
group = "dev.academy.migration"
version = "1.0.0"
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(17)
}
dependencies {
implementation("org.apache.commons:commons-lang3:3.20.0")
testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
}
tasks.test {
useJUnitPlatform()
}
tasks.jar {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
pom {
name.set("Greeting Library")
}
}
}
}
Copy identical GreetingFormatter.java and
GreetingFormatterTest.java into both source trees.
Before execution, predict:
- both builds will select Commons Lang 3.20.0 and run two tests;
- the application class bytes should match under the same compiler/toolchain contract, but whole-JAR SHA-256 may differ because archive metadata/model implementation differs;
-
the Gradle-generated Maven POM may expose Commons Lang differently
because the candidate uses
implementation.
4. Build the authoritative Maven baseline and capture evidence
From maven/:
mkdir -p ../evidence/maven
./mvnw --version | tee ../evidence/maven/tool.txt
java -version 2> ../evidence/maven/java.txt
./mvnw -Dmaven.repo.local="$PWD/.checkpoint-m2" clean verify | tee ../evidence/maven/build.log
./mvnw -Dmaven.repo.local="$PWD/.checkpoint-m2" \
org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree \
> ../evidence/maven/dependency-tree.txt
jar tf target/greeting-lib-1.0.0.jar > ../evidence/maven/jar-entries.txt
sha256sum target/greeting-lib-1.0.0.jar > ../evidence/maven/jar.sha256
cp target/surefire-reports/*.xml ../evidence/maven/
This evidence is the rollback anchor. Do not delete or rewrite it when the candidate fails.
5. Build the Gradle candidate and capture equivalent evidence
From gradle/:
mkdir -p ../evidence/gradle
export GRADLE_USER_HOME="$PWD/.checkpoint-gradle"
./gradlew --version | tee ../evidence/gradle/tool.txt
java -version 2> ../evidence/gradle/java.txt
./gradlew clean build | tee ../evidence/gradle/build.log
./gradlew dependencies --configuration runtimeClasspath > ../evidence/gradle/dependency-tree.txt
./gradlew dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath \
> ../evidence/gradle/dependency-insight.txt
./gradlew generatePomFileForMavenJavaPublication
jar tf build/libs/greeting-lib-1.0.0.jar > ../evidence/gradle/jar-entries.txt
sha256sum build/libs/greeting-lib-1.0.0.jar > ../evidence/gradle/jar.sha256
cp build/publications/mavenJava/pom-default.xml ../evidence/gradle/generated-pom.xml
cp build/test-results/test/*.xml ../evidence/gradle/
If the Gradle candidate cannot run, that is a checkpoint result—not permission to alter the Maven baseline or unpin versions.
6. Compare evidence by invariant
Dependency version:
grep -n "commons-lang3" evidence/maven/dependency-tree.txt || true
grep -n "commons-lang3" evidence/gradle/dependency-tree.txt || true
Test counts:
grep -h "tests=\"" evidence/maven/*.xml | head -n 5
grep -h "tests=\"" evidence/gradle/*.xml | head -n 5
JAR entries and hashes:
diff -u evidence/maven/jar-entries.txt evidence/gradle/jar-entries.txt || true
cat evidence/maven/jar.sha256
cat evidence/gradle/jar.sha256
Generated consumer metadata:
grep -nE "groupId|artifactId|version|scope" maven/pom.xml
grep -nE "groupId|artifactId|version|scope" evidence/gradle/generated-pom.xml
7. Independently compare compiled application class bytes
Extract each JAR and compare the class that implements the public behavior:
rm -rf evidence/unpacked && mkdir -p evidence/unpacked/maven evidence/unpacked/gradle
(cd evidence/unpacked/maven && jar xf ../../../maven/target/greeting-lib-1.0.0.jar)
(cd evidence/unpacked/gradle && jar xf ../../../gradle/build/libs/greeting-lib-1.0.0.jar)
sha256sum evidence/unpacked/maven/dev/academy/migration/GreetingFormatter.class
sha256sum evidence/unpacked/gradle/dev/academy/migration/GreetingFormatter.class
javap -verbose evidence/unpacked/maven/dev/academy/migration/GreetingFormatter.class \
| grep "major version"
javap -verbose evidence/unpacked/gradle/dev/academy/migration/GreetingFormatter.class \
| grep "major version"
Expected target is class-file major 61 for Java 17. If application class bytes match but whole-JAR hashes differ, the non-equivalence is likely packaging metadata; inspect manifests/entry timestamps before deciding whether byte identity is required by your release process.
8. Inject a dependency mismatch and prove the gate catches it
Change only the Gradle candidate's Commons Lang dependency from
3.20.0 to 3.19.0. Predict: the Gradle
dependency graph changes while the Maven baseline does not. Capture
the mismatch:
# After intentionally editing only gradle/build.gradle.kts:
cd gradle
export GRADLE_USER_HOME="$PWD/.checkpoint-gradle"
./gradlew dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath \
> ../evidence/gradle/dependency-insight-broken.txt
cd ..
grep -n "commons-lang3" evidence/maven/dependency-tree.txt || true
grep -n "commons-lang3" evidence/gradle/dependency-insight-broken.txt || true
Do not repair with an exclusion or forced resolution rule merely to
hide the evidence. Restore the intended 3.20.0 declaration, rerun
dependencyInsight, then rebuild the candidate in
controlled state.
9. Prove the CI entry point preserves the required outcome
Record the intended commands in a small repository-owned migration note:
Maven authoritative lane:
./mvnw -Dmaven.repo.local=.ci-m2 clean verify
Gradle candidate lane:
GRADLE_USER_HOME=.ci-gradle ./gradlew clean build
Required evidence from either lane:
- exact Wrapper/build-tool/JDK identity
- dependency graph
- two-test report set
- packaged JAR
- JAR checksum
- Maven-consumer publication metadata
Now deliberately run ./gradlew clean check in a clean
candidate workspace and verify that you do not assume packaging from
the task name. Restore build as the candidate CI entry
point when packaging is part of the contract.
10. Write the non-equivalence register
A professional migration report explicitly lists accepted and rejected differences. Example:
| Difference | Observed? | Disposition |
|---|---|---|
| Maven and Gradle dependency report formatting differs. | Yes | Accept; selected coordinate/version and consumer graph are the invariant. |
| Whole JAR SHA-256 differs. | Possible | Investigate manifest/archive metadata; do not dual-publish same immutable version. Decide whether byte identity is required. |
| Compiled application class SHA-256 differs. | Should not without explanation | Block migration until compiler/toolchain/source/generated-input difference is understood. |
Gradle generated POM narrows Commons Lang to runtime due
implementation.
|
Expected in this fixture | Accept only after clean consumer/public API verification. |
| Test count differs. | No | Block migration; false-green risk. |
| Gradle adds Module Metadata if published. | Expected Gradle capability | Accept if Maven consumers remain supported and richer semantics do not become hidden mandatory requirements. |
11. Rollback plan before cutover
Rollback is simple because the Maven baseline was never destroyed:
- keep Maven Wrapper/POM/CI lane authoritative;
- keep Gradle files/candidate CI lane isolated or remove them from the migration branch;
- do not publish any Gradle-built candidate under the production release coordinate;
- retain evidence explaining why the candidate was rejected;
- fix one semantic mismatch at a time and rerun the checklist.
At cutover time, switch the single publication writer explicitly. Rollback then means restoring that writer and the old verified build lane—not recreating old build logic from memory.
12. Produce a justified tool-selection recommendation
Finish with one of three recommendations: stay on Maven, migrate to Gradle, or operate a governed mixed estate. Justify it with evidence in this format:
| Question | Evidence to cite |
|---|---|
| Does the candidate preserve behavior? | Dependency/test/class/metadata/CI equivalence checklist. |
| Is there a measured benefit? | Representative local/CI timings with controlled cache/toolchain/test state. |
| What new complexity appears? | Gradle convention plugins/caches/variant metadata, or Maven plugin/lifecycle constraints. |
| Can the platform support it? | Team skill, wrapper/JDK baselines, plugin support, security/repository policy. |
| What is the rollback cost? | Single publication writer, side-by-side build retention, reversible CI cutover. |
| What is the long-term governance plan? | Version support windows, upgrade testing, artifact interoperability, shared delivery controls. |
13. Final verification checklist
| Evidence | Pass condition |
|---|---|
| Exact files/models | Maven POM and Gradle settings/build scripts reviewed; no hidden production settings/init scripts. |
| Tool identity | Maven 3.9.16 / Wrapper 3.3.4 and Gradle 9.7.1 Wrapper evidence captured; JDK 21 recorded. |
| Compatibility | Both compile with Java 17 target; class major version verified. |
| Dependencies | Commons Lang 3.20.0 after repair; no unexplained graph drift. |
| Tests | Two tests in each lane; no skipped/filtered-zero false green. |
| Artifact | Required class payload present; raw checksums recorded; differences explained. |
| Metadata | Same GAV; consumer scope differences intentional and tested. |
| CI | Maven verify versus Gradle build mapping documented by required outcomes. |
| Publication | One authoritative writer for immutable coordinate. |
| Rollback | Old verified build and CI path remains recoverable until cutover acceptance. |
14. Cleanup and bridge to Chapter 31
Delete only disposable checkpoint caches/workspaces after preserving the evidence report:
rm -rf maven/.checkpoint-m2 gradle/.checkpoint-gradle evidence/unpacked
# Remove the entire checkpoint workspace only when you no longer need its evidence.
# rm -rf ch30-checkpoint
What Chapter 30 adds: you can now treat build-tool migration as an engineering change with explicit invariants, evidence, rollback, and governance. Chapter 31 combines the whole course into a production capstone: build, test, secure, publish, and optimize a multi-module JVM platform with an operational handoff.
Knowledge check
Why must invariants be written before migration edits?
They prevent the target build syntax from redefining “success” after the fact and give rollback/acceptance objective criteria.
Which mismatch is intentionally injected in the checkpoint?
The Gradle candidate is changed to Commons Lang 3.19.0 while the Maven baseline remains at 3.20.0; dependency evidence must expose it.
Can behavioral equivalence be accepted if the whole-JAR SHA-256 differs?
Possibly, but only after the difference is explained and the release policy accepts it. Never publish two different binaries under one immutable coordinate.
Which difference would normally block the migration immediately?
Different required test coverage/counts or unexplained compiled application class differences, because they indicate behavior/toolchain/input drift.
What makes rollback low risk in this checkpoint?
The Maven baseline and publication authority are preserved until the Gradle candidate passes all invariants.
What does Chapter 31 build on top of this?
An integrated production operating model spanning build structure, dependencies, tests, security, publishing, CI, performance, and handoff.
Official references and version notes
- Apache Maven release history — Maven 3.9.16 GA baseline; Maven 4.0.0-rc-6 remains pre-GA at generation time.
- Apache Maven Wrapper 3.3.4 — current stable Wrapper baseline.
- Maven build lifecycle — lifecycle phases and plugin-goal execution model.
- Maven dependency mechanism — mediation, scopes, dependency management, and BOM concepts.
- Maven Compiler Plugin 3.15.0 — pinned compiler-plugin baseline.
- Maven Surefire 3.5.6 — pinned unit-test execution baseline.
- Maven JAR Plugin 3.5.1 — pinned JAR packaging baseline.
- Gradle 9.7.1 release notes — pinned Gradle baseline.
- Migrating builds from Apache Maven — side-by-side migration and semantic-difference guidance.
- Gradle Build Init plugin — Maven POM conversion support and its limitations.
- Gradle dependency management — configuration/variant-aware resolution model.
- Gradle Maven Publish — generated POM/publication semantics.
Version-sensitive statements were rechecked against primary documentation on 2026-08-24. The mandatory path remains local/free; no hosted CI, repository manager, commercial analytics service, or production credentials are required.
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.