This guide provides step-by-step instructions for migrating JVM microservices to:
- Java 25 (from Java 17)
- SpringBoot 4.0 (from SpringBoot 3.x)
| Step | Description | Guide |
|---|---|---|
| 0 | Add Integration Tests (CRITICAL!) | 05-testing-validation.md |
| 1 | Java 17 → Java 21 | 01-java-upgrade.md |
| 2 | Java 21 → Java 25 | 01-java-upgrade.md |
| 3 | SpringBoot 3.4.x → SpringBoot 3.5.x | 02-springboot-upgrade.md |
| 4 | SpringBoot 3.5.x → SpringBoot 4.0 | 02-springboot-upgrade.md |
Before any migration, ensure you have comprehensive integration tests.
Without integration tests:
- You can't verify the migration didn't break anything
- Breaking changes in Java/Spring versions go undetected
- Rollback becomes much harder
Create an integration tests PR FIRST, merge it, then rebase migration branches.
- Repository operations (database CRUD)
- API endpoints (REST controllers)
- Service initialization (Spring context)
- Core business logic
Each migration step must be done in a separate PR using stacked branches. Do not combine multiple migration steps in a single PR.
main
└── chore/add-integration-tests (PR #0: Add tests - MERGE FIRST!)
main (with tests merged)
└── chore/java-21-migration (PR #1: Java 17 → 21)
└── chore/java-25-migration (PR #2: Java 21 → 25)
└── chore/springboot-3.5 (PR #3: SpringBoot 3.4 → 3.5)
└── chore/springboot-4.0 (PR #4: SpringBoot 3.5 → 4.0)
- Create integration tests PR → Merge to main
- Rebase migration branches onto main (with tests)
- Merge migration PRs in order
# Step 0: Create integration tests branch
git checkout main
git checkout -b chore/add-integration-tests
# Add tests, commit, push
git push -u origin chore/add-integration-tests
gh pr create --base main --title "test: add integration tests"
# MERGE THIS FIRST before continuing!
# After tests PR is merged:
git checkout main
git pull origin main
# Step 1: Create Java 21 branch from main (now has tests)
git checkout -b chore/java-21-migration
# Make changes, commit, push
git push -u origin chore/java-21-migration
gh pr create --base main --title "chore: migrate to Java 21"
# Step 2: Create Java 25 branch from Java 21 branch
git checkout -b chore/java-25-migration
# Make changes, commit, push
git push -u origin chore/java-25-migration
gh pr create --base chore/java-21-migration --title "chore: migrate to Java 25"
# Continue pattern for SpringBoot upgrades...Merge in order from bottom to top:
- Merge PR #1 (Java 21) into main
- Rebase PR #2 onto main, then merge
- Rebase PR #3 onto main, then merge
- Rebase PR #4 onto main, then merge
This ensures:
- Easy rollback if issues are found
- Clear git history
- Proper CI/CD validation at each step
Before starting migration:
- Integration tests exist and pass (CRITICAL!)
- VPN connected
- Authenticated with your staging/cluster environment (per your org's process)
- Authenticated with your artifact registry (per your org's process)
- Docker running (for integration tests)
- SDKMAN installed for Java version management
# List available versions
sdk list java
# Install specific version
sdk install java 21.0.9-amzn
sdk install java 25.0.1-amzn
# Switch version
sdk use java 21.0.9-amzn# Full build with tests
./gradlew clean build
# Unit tests only
./gradlew test
# Integration tests
./gradlew integrationTest
# Build with deprecation warnings
./gradlew build --warning-mode all# Rebase after base branch is merged
git checkout chore/java-25-migration
git rebase origin/main # or origin/chore/java-21-migration
git push --force-with-lease| File | Description |
|---|---|
| 01-java-upgrade.md | Java upgrade steps |
| 02-springboot-upgrade.md | SpringBoot upgrade steps |
| 03-gradle-upgrade.md | Gradle version requirements |
| 04-dependencies-checklist.md | Dependency compatibility |
| 05-testing-validation.md | Testing and validation guide |
| 99-migration-log.md | Migration learnings log |
# List available Java versions (use -amzn versions for AWS compatibility)
sdk list java
# Install Amazon Corretto versions (preferred for AWS compatibility)
sdk install java 21.0.9-amzn # Java 21
sdk install java 25.0.1-amzn # Java 25
# Switch Java version
sdk use java 21.0.9-amzn # Temporary (current shell)
sdk default java 21.0.9-amzn # Permanent defaultEach migration step must be done in a separate PR:
- Java 17 → 21: Create PR, get review, merge
- Java 21 → 25: Create PR, get review, merge
- SpringBoot 3.4.x → 3.5.x: Create PR, get review, merge
- SpringBoot 3.5.x → 4.0: Create PR, get review, merge
PR titles must follow conventional commits format: chore: migrate to Java 25
This ensures:
- Easier rollback if issues arise
- Clear history of changes
- Proper CI/CD validation at each step
- Easier debugging of any regressions
| File | Change |
|---|---|
.java-version |
17 -> 21 |
Dockerfile |
eclipse-temurin:17-jre-alpine -> eclipse-temurin:21-jre-alpine |
Note: build.gradle reads from .java-version automatically, no changes needed there.
-
Update
.java-version21 -
Update
DockerfileFROM public.ecr.aws/docker/library/eclipse-temurin:21-jre-alpine -
Install and use Java 21 locally
sdk install java 21.0.5-amzn sdk use java 21.0.5-amzn java -version # Verify -
Build and test
./gradlew clean build
- Virtual Threads (JEP 444) - Production ready
- Sequenced Collections (JEP 431) - New interfaces
- Record Patterns (JEP 440) - Enhanced pattern matching
- String Templates (Preview) - Not recommended for production yet
- Deprecations removed - Some APIs removed that were deprecated in 17
| Issue | Solution |
|---|---|
--add-opens warnings |
Add JVM args or update libraries |
| Reflection access errors | Update to Java 21 compatible library versions |
| SecurityManager usage | Deprecated, plan removal |
| File | Change |
|---|---|
.java-version |
21 -> 25 |
Dockerfile |
eclipse-temurin:21-jre-alpine -> eclipse-temurin:25-jre-alpine |
scripts/assemble.sh |
./gradlew docker -> ./gradlew jibDockerBuild |
-
Update
.java-version25 -
Update
Dockerfile(if not using Jib for everything)FROM public.ecr.aws/docker/library/eclipse-temurin:25-jre-alpine -
Install and use Java 25 locally
sdk install java 25.0.1-amzn sdk use java 25.0.1-amzn java -version
-
Build and test
./gradlew clean build
- Further preview features promoted to standard
- Performance improvements
- Check release notes for specific deprecation removals
Java 25 requires Gradle 9.x because Gradle 8.x doesn't support class file version 69.
This means you need to update all Gradle plugins to Gradle 9 compatible versions BEFORE migrating to Java 25.
| Issue | Solution |
|---|---|
Unsupported class file major version 69 |
Upgrade to Gradle 9.3.1+ |
Lombok fails with NoSuchFieldException |
Update Lombok to 1.18.40+ |
com.palantir.docker fails |
Replace with Google Jib 3.5.2 |
Avro plugin JavaPluginConvention error |
Update to davidmc24 1.9.1 |
| JaCoCo instrumentation error | Update to JaCoCo 0.8.14+ |
| ByteBuddy/Mockito Java 25 error | Add -Dnet.bytebuddy.experimental=true to test JVM args |
| internal Gradle plugin JacocoPlugin error | Remove plugin, add jacoco plugin directly |
integrationTest shows NO-SOURCE |
Add explicit testClassesDirs and classpath config (see below) |
CI Task 'docker' not found |
Update scripts/assemble.sh to use jibDockerBuild |
| PR title validation fails | Use conventional commits format: chore: migrate to Java 25 |
| Plugin | Before | After |
|---|---|---|
| Gradle | 7.6.4 | 9.3.1 |
| Lombok | 1.18.30 | 1.18.40 |
| JaCoCo | 0.8.9 | 0.8.14 |
| com.palantir.docker | 0.36.0 | Replace with Jib 3.5.2 |
| gradle-avro-plugin | commercehub 0.22.0 | davidmc24 1.9.1 |
| docker-compose | 0.17.12 | 0.17.20 |
| internal/org-specific plugin | (varies) | Remove (add jacoco directly) |
plugins {
id 'com.google.cloud.tools.jib' version '3.5.2'
}
jib {
from {
image = 'public.ecr.aws/docker/library/eclipse-temurin:25-jre-alpine'
}
to {
image = 'your-image-name'
tags = ['latest']
}
container {
jvmFlags = ['-Djava.security.egd=file:/dev/./urandom', '-XX:InitialRAMPercentage=70.0', '-XX:MaxRAMPercentage=70.0']
ports = ['8080']
mainClass = 'com.your.MainClass'
}
}When replacing Palantir Docker with Jib, update the assemble script:
#!/bin/sh
# Before (Palantir Docker):
# ./gradlew docker --no-daemon -x check -x composeUp --info --gradle-user-home ${HOME}/.gradle
# After (Jib):
./gradlew jibDockerBuild --no-daemon -x check -x composeUp --info --gradle-user-home ${HOME}/.gradleCRITICAL: Gradle 9.x requires explicit test class directories and classpath configuration for custom Test tasks.
Without this fix, integrationTest will show NO-SOURCE and skip all integration tests.
Update testing.gradle:
task integrationTest(type: Test) {
// Gradle 9.x requires explicit test class directories and classpath configuration
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'integration-test'
}
// ... rest of configuration
}Add to test configurations:
test {
useJUnitPlatform()
jvmArgs '-Dnet.bytebuddy.experimental=true'
}
integrationTest {
jvmArgs '-Dnet.bytebuddy.experimental=true'
}SpringBoot 3.4.x -> 3.5.x -> 4.0
Important: Always upgrade incrementally. Do not skip versions.
| File | Change |
|---|---|
build.gradle |
springBootVersion = '3.4.5' -> springBootVersion = '3.5.x' |
-
Update
build.gradlespringBootVersion = '3.5.0' // Or latest 3.5.x
-
Update Spring Cloud (if needed)
springCloudVersion = '2025.0.0' // Check compatibility
-
Update Hibernate dialect (recommended before 4.0)
# application.yml spring: jpa: properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect # Not PostgreSQL95Dialect
-
Build and test
./gradlew clean build
Check: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.5-Release-Notes
- Configuration property changes
- Dependency upgrades
- New features and deprecations
| Issue | Solution |
|---|---|
| Property renamed | Check release notes for property mappings |
| Deprecated API | Update to new API before 4.0 |
| Hibernate dialect warning | Use PostgreSQLDialect instead of versioned dialects |
| File | Change |
|---|---|
build.gradle |
springBootVersion = '3.5.x' -> springBootVersion = '4.0.x' |
build.gradle |
Update springCloudVersion to 2025.1.x |
build.gradle |
Update incompatible dependencies |
| Test files | @MockBean -> @MockitoBean |
-
Update
build.gradlespringBootVersion = '4.0.2' springCloudVersion = '2025.1.1' // NOT 2026.x - see note below
IMPORTANT: Despite some documentation suggesting Spring Cloud 2026.x, Spring Boot 4.0.x works with Spring Cloud 2025.1.x. Check Spring Cloud Supported Versions
-
Update modularized starters
// Old (3.5.x) implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-batch' implementation 'org.flywaydb:flyway-core' // New (4.0.x) implementation 'org.springframework.boot:spring-boot-starter-aspectj' // renamed implementation 'org.springframework.boot:spring-boot-starter-batch-jdbc' // modularized implementation 'org.springframework.boot:spring-boot-starter-flyway' // use starter // For @WebMvcTest in tests testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
-
Update test annotations
// Old (3.5.x) import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; // New (4.0.x) import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
-
Update package imports
// DataSourceProperties moved // Old: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties // New: org.springframework.boot.jdbc.autoconfigure.DataSourceProperties // Spring Batch infrastructure moved // Old: org.springframework.batch.item.* // New: org.springframework.batch.infrastructure.item.*
-
Refactor Spring Batch code (if using custom readers)
Spring Batch 6.0 removes setters from item readers. Must use builders:
// BROKEN in Spring Batch 6.0 JdbcCursorItemReader<T> reader = new JdbcCursorItemReader<>(); reader.setDataSource(dataSource); reader.setSql(sql); // CORRECT for Spring Batch 6.0 JdbcCursorItemReader<T> reader = new JdbcCursorItemReaderBuilder<T>() .dataSource(dataSource) .sql(sql) .rowMapper(rowMapper) .build();
-
Build and test
./gradlew clean build
Full guide: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
- Java 17+ required (Java 25 recommended)
- Gradle 8.10+ required (Gradle 9.x for Java 25)
| Old Package | New Package |
|---|---|
o.s.boot.test.mock.mockito.MockBean |
o.s.test.context.bean.override.mockito.MockitoBean |
o.s.boot.test.autoconfigure.web.servlet.WebMvcTest |
o.s.boot.webmvc.test.autoconfigure.WebMvcTest |
o.s.boot.autoconfigure.jdbc.DataSourceProperties |
o.s.boot.jdbc.autoconfigure.DataSourceProperties |
o.s.batch.item.* |
o.s.batch.infrastructure.item.* |
| Old Starter | New Starter |
|---|---|
spring-boot-starter-aop |
spring-boot-starter-aspectj |
spring-boot-starter-batch |
spring-boot-starter-batch-jdbc (for JDBC) |
flyway-core |
spring-boot-starter-flyway |
| (none) | spring-boot-starter-webmvc-test (for @WebMvcTest) |
- All item readers/writers use builder pattern only
- Setters removed from
JdbcCursorItemReader,JdbcPagingItemReader, etc. - Must refactor any code that extends these classes
| SpringBoot | Spring Cloud | Notes |
|---|---|---|
| 3.4.x | 2024.0.x | Current stable |
| 3.5.x | 2025.0.x | |
| 4.0.x | 2025.1.x | Not 2026.x! |
Note: Spring Cloud 2026.x does not exist yet as of Feb 2026. Spring Boot 4.0.x is compatible with Spring Cloud 2025.1.x.
# Run with deprecation warnings
./gradlew bootRun --args='--spring.config.additional-location=optional:file:./deprecated-check.yml'| Old Property | New Property |
|---|---|
spring.dao.exceptiontranslation.enabled |
spring.persistence.exceptiontranslation.enabled |
management.tracing.enabled |
management.tracing.export.enabled |
Check the official migration guide for complete property renames.
After each upgrade:
-
./gradlew clean buildpasses - Unit tests pass:
./gradlew test - Integration tests pass:
./gradlew integrationTest - Application starts locally
- No deprecation warnings (or documented)
- Health endpoint responds:
/actuator/health - For data/ETL services: Compare output between versions (reset consumer offsets before each export)
| Java Version | Min Gradle | Recommended |
|---|---|---|
| 17 | 7.3+ | 7.6.4 |
| 21 | 8.4+ | 8.10+ |
| 25 | 8.10+ | 8.12+ |
Check current version:
./gradlew --versionCheck wrapper properties:
cat gradle/wrapper/gradle-wrapper.properties-
Update wrapper
./gradlew wrapper --gradle-version=8.12
-
Verify update
./gradlew --version
-
Test build
./gradlew clean build
Edit gradle/wrapper/gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zipThen run:
./gradlew wrapper| Plugin | Current | Java 21 | Java 25 |
|---|---|---|---|
org.springframework.boot |
3.4.5 | ✓ | 4.0+ |
io.spring.dependency-management |
1.1.7 | ✓ | Check |
com.palantir.docker |
0.36.0 | ✓ | Check |
com.avast.gradle.docker-compose |
0.17.12 | ✓ | Check |
org.sonarqube |
7.2.0.6526 | ✓ | Check |
org.flywaydb.flyway |
11.8.2 | ✓ | Check |
| internal/org-specific plugin | (varies) | ✓ | Check |
In build.gradle plugins block:
plugins {
id 'org.springframework.boot' version '4.0.0'
// ... other plugins
}- Configuration cache - May require code changes
- Build scan - Updated integration
- Java toolchain - Better support
Instead of setting sourceCompatibility/targetCompatibility, use toolchains:
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}This ensures consistent Java version across different machines.
Error: Plugin X requires Gradle Y
Solution: Update the plugin version in build.gradle
Error: Various deprecation warnings during build
Solution:
./gradlew build --warning-mode allThen address each warning.
Error: Configuration cache state could not be cached
Solution: Update plugins or disable configuration cache temporarily:
./gradlew build --no-configuration-cacheAfter Gradle upgrade:
# Verify version
./gradlew --version
# Clean build
./gradlew clean build
# Check for warnings
./gradlew build --warning-mode all
# Run tests
./gradlew test| Dependency | Current | Java 21 | Java 25 / SB 4.0 | Notes |
|---|---|---|---|---|
| SpringBoot | 3.4.5 | ✓ | 4.0.x | Major upgrade required |
| Spring Cloud | 2024.0.1 | ✓ | 2026.0.x | Update with SpringBoot |
| Spring Dependency Management | 1.1.7 | ✓ | Check | May need update |
| Dependency | Current | Java 21 | Java 25 / SB 4.0 | Notes |
|---|---|---|---|---|
| PostgreSQL Driver | 42.7.5 | ✓ | ✓ | Usually compatible |
| Flyway | 11.8.2 | ✓ | Check | May need update |
| Dependency | Current | Java 21 | Java 25 / SB 4.0 | Notes |
|---|---|---|---|---|
| Kafka Clients | 2.6.0 | Very old, update recommended | ||
| Kafka Avro Serde | 5.3.0 | Very old, check Confluent versions | ||
| Avro | 1.12.0 | ✓ | ✓ | Recent version |
| Spring Cloud AWS Messaging | 2.2.6.RELEASE | ❌ | EOL, migrate to AWS SDK v2 |
| Dependency | Current | Java 21 | Java 25 / SB 4.0 | Notes |
|---|---|---|---|---|
| Lombok | 1.18.30 | ✓ | Check | Java 25 support TBD |
| Commons IO | 2.19.0 | ✓ | ✓ | Recent version |
| Commons Lang3 | 3.17.0 | ✓ | ✓ | Recent version |
| Commons Collections4 | 4.5.0 | ✓ | ✓ | Recent version |
| Dependency | Current | Java 21 | Java 25 / SB 4.0 | Notes |
|---|---|---|---|---|
| Logback | 1.5.3 | ✓ | ✓ | Managed by Spring |
| Logback Contrib | 0.1.5 | Old, check alternatives |
| Dependency | Current | Java 21 | Java 25 / SB 4.0 | Notes |
|---|---|---|---|---|
| JUnit Jupiter | 5.11.3 | ✓ | ✓ | Recent version |
| Mockito | Managed | ✓ | ✓ | Spring managed |
| Awaitility | 4.3.0 | ✓ | ✓ | Recent version |
| Hamcrest | 3.0 | ✓ | ✓ | Recent version |
Status: End of Life
Problem: Old version, not compatible with newer Spring Boot
Solution:
- Migrate to
io.awspring.cloud:spring-cloud-aws-starter-sqs(AWS SDK v2) - Or use
software.amazon.awssdk:sqs
// Old
implementation 'org.springframework.cloud:spring-cloud-aws-messaging:2.2.6.RELEASE'
// New
implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs:3.x.x'Status: Very old (2020)
Problem: May have compatibility issues with newer Java
Solution: Update to 3.x:
kafkaClientVersion = '3.7.0'Status: Very old
Problem: Tied to old Kafka version
Solution: Update to match Kafka clients version
-
Before Java 21:
- Update Kafka clients to 3.x
- Update Confluent dependencies
- Consider Spring Cloud AWS migration
-
Before SpringBoot 4.0:
- Complete Spring Cloud AWS migration
- Update all Spring Cloud dependencies
- Verify Lombok Java 25 support
# List all dependencies
./gradlew dependencies
# Check for updates (if using versions plugin)
./gradlew dependencyUpdates
# Check specific configuration
./gradlew dependencies --configuration runtimeClasspath| Dependency | Issue | Workaround |
|---|---|---|
spring-cloud-aws-messaging |
Not maintained | Migrate to awspring |
logback-contrib |
Potentially unmaintained | Check for alternatives |
| Kafka 2.x | Old API | Update to 3.x |
Before starting ANY migration, ensure you have comprehensive integration tests that verify:
- Database operations - All repository methods work correctly
- API endpoints - REST controllers respond as expected
- Application startup - Spring context initializes properly
- Business logic - Core services function correctly
Why is this important?
- Integration tests catch issues that unit tests miss
- They verify the entire stack works together (DB → Service → API)
- Breaking changes in Java/Spring versions often surface in integration tests
- Without a test baseline, you can't verify the migration didn't break anything
Add integration tests PR BEFORE migration PRs, then rebase migration branches after tests are merged.
Before starting any migration:
- Integration tests exist and pass (CRITICAL!)
- VPN connected
- Authenticated with your cluster environment (per your org's process)
- Artifact registry access configured
- Docker running
- Clean working directory:
git status - On feature branch
- Correct Java version:
java -version
./gradlew clean buildThis runs:
- Compilation
- Unit tests
- Integration tests (requires Docker)
./gradlew clean build --warning-mode allShows deprecation warnings - important for migration planning.
./gradlew testNote: Unit tests should NOT require Docker.
./gradlew integrationTestRequires: Docker running (for PostgreSQL container)
Integration tests are tagged with @Tag("integration-test") and run with docker-compose.
./gradlew integrationTest --rerun-tasks./gradlew test integrationTest --info./gradlew test --tests "com.example.service.SomeTest"
./gradlew integrationTest --tests "*RepositoryIT"- Located in
src/test/java - Class names:
*Test.java - No Docker/external dependencies
- Mock all external services
- Located in
src/test/java - Class names:
*IT.java - Tagged with
@Tag("integration-test") - Use
@SpringBootTestwith@ActiveProfiles("test") - Require Docker for PostgreSQL
testing.gradle:
test {
useJUnitPlatform {
excludeTags 'integration-test'
}
filter {
excludeTestsMatching '*IT' // Prevent Spring context init
}
}
task integrationTest(type: Test) {
useJUnitPlatform {
includeTags 'integration-test'
}
}Problem: Integration tests fail with connection errors
Check:
# Docker running?
docker ps
# Port available?
lsof -i :5432
# Docker Compose works?
cd docker && docker compose up -d && docker compose downProblem: Failed to load ApplicationContext
Common causes:
- Missing
@MockBeanfor Kafka-related beans - Database not available
- Missing test configuration
Solution: Check error log for the specific bean that failed.
Problem: Tests not found or wrong tests run
Check:
# See what tests will run
./gradlew test --dry-run
./gradlew integrationTest --dry-run
# Check test output location
ls -la build/test-results/test/
ls -la build/test-results/integrationTest/Problem: OutOfMemoryError during tests
Solution: In gradle.properties:
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g./gradlew bootRun./gradlew bootRun --args='--spring.profiles.active=local'Once running, verify:
curl http://localhost:8080/actuator/healthExpected response:
{"status":"UP"}./gradlew jibDockerBuild./gradlew dockerdocker run your-image-name java -versionAfter EACH migration step:
-
./gradlew clean build- Passes -
./gradlew test- All unit tests pass -
./gradlew integrationTest- All integration tests pass
-
./gradlew bootRun- Starts without errors -
/actuator/health- Returns UP - No unexpected deprecation warnings in logs
- Docker image builds successfully
- Container starts and runs
- No new compiler warnings
-
./gradlew build --warning-mode all- Review warnings
- Push to branch
- CI pipeline passes
- All checks green before merging
If migration fails:
# Discard changes
git checkout .
# Or reset to last commit
git reset --hard HEAD
# Switch back Java version (using SDKMAN)
sdk use java 17.0.17-amznBefore migration, record:
# Build time
time ./gradlew clean build
# Test time
time ./gradlew test
time ./gradlew integrationTest
# Startup time (check logs)
./gradlew bootRunCompare after migration to detect regressions.
This is a living document recording learnings from each repository migration.
## [Repo Name] - [Date]
### Migration: [From] -> [To]
**Duration:** X hours
### Changes Made
-
### Issues Encountered
| Issue | Error Message | Solution | Time Spent |
|-------|--------------|----------|------------|
| | | | |
### Commands Run
\`\`\`bash
# Commands here
\`\`\`
### Notes for Next Migration
-
---Duration: ~30 minutes (including CI fix)
Status: SUCCESS (merged)
.java-version: 17 -> 21Dockerfile: eclipse-temurin:17-jre-alpine -> eclipse-temurin:21-jre-alpinegradle-wrapper.properties: Gradle 7.6.4 -> 8.10.2 (required for Java 21).github/workflows/pr.yaml: java_version: '17' -> '21'.github/workflows/main.yaml: java_version: '17' -> '21' (3 places)
| Issue | Error Message | Solution | Time Spent |
|---|---|---|---|
| Gradle 7.6.4 doesn't support Java 21 | Unsupported class file major version 65 |
Upgrade to Gradle 8.10.2 | 5 min |
| CI still using Java 17 | Tests failing with version mismatch | Update hardcoded java_version in workflows | 10 min |
| Deprecated bootJar properties | Could not get unknown property 'archiveName' |
Use archiveFile and archiveFileName.get() |
5 min |
# Install and switch to Java 21
sdk install java 21.0.9-amzn
sdk use java 21.0.9-amzn
java -version
# Build and test
./gradlew clean build- All 19 unit tests passed
- 1 integration test passed (1 skipped)
- Build successful in 23s
- Always check CI workflow files for hardcoded Java versions
- Gradle 8.x is required for Java 21+ (major version 65)
- Check for deprecated Gradle API usage (
archiveName->archiveFileName.get())
Duration: ~2 hours (including multiple CI fixes)
Status: SUCCESS (merged)
.java-version: 21 -> 25Dockerfile: eclipse-temurin:21-jre-alpine -> eclipse-temurin:25-jre-alpinegradle-wrapper.properties: Gradle 8.10.2 -> 9.3.1build.gradle:- Lombok: 1.18.30 -> 1.18.40
- JaCoCo: 0.8.9 -> 0.8.14
- Avro plugin: com.commercehub 0.22.0 -> com.github.davidmc24 1.9.1
- Docker plugin: com.palantir.docker -> com.google.cloud.tools.jib 3.5.2
- docker-compose: 0.17.12 -> 0.17.20
- Removed: internal/org-specific Gradle plugin (incompatible with Gradle 9)
- Added: jacoco plugin directly
- Added: ByteBuddy experimental flag for tests
SomeEntity.avsc: Fixed malformed logicalType placementSomeEntityMapper.java: Updated to use ListSomeEntityMapperTest.java: Updated assertions for UUIDtesting.gradle:- Added ByteBuddy experimental flag for integration tests
- Added explicit
testClassesDirsandclasspathfor Gradle 9.x compatibility
scripts/assemble.sh: Changed./gradlew dockerto./gradlew jibDockerBuild.github/workflows/pr.yaml: java_version: '21' -> '25'.github/workflows/main.yaml: java_version: '21' -> '25' (3 places)
| Issue | Error Message | Solution | Time Spent |
|---|---|---|---|
| Lombok incompatibility | NoSuchFieldException: com.sun.tools.javac.code.TypeTag |
Update Lombok to 1.18.40 | 5 min |
| Gradle 8.x doesn't support Java 25 | Unsupported class file major version 69 |
Upgrade to Gradle 9.3.1 | 5 min |
| Palantir Docker plugin incompatible | Could not create plugin PalantirDockerPlugin |
Replace with Google Jib | 10 min |
| Internal Gradle plugin incompatible | JacocoPlugin closure invalid |
Remove plugin, add jacoco directly | 5 min |
| Avro plugin incompatible | org/gradle/api/plugins/JavaPluginConvention |
Update to 1.9.1 (new coordinates) | 5 min |
| Malformed Avro schema | List<String> cannot be converted to UUID |
Fix logicalType placement in schema | 10 min |
| JaCoCo 0.8.9 doesn't support Java 25 | Error while instrumenting |
Update to 0.8.14 | 2 min |
| ByteBuddy/Mockito Java 25 | Java 25 (69) is not supported by Byte Buddy |
Add -Dnet.bytebuddy.experimental=true |
3 min |
| CI build-docker-image fails | Task 'docker' not found in root project |
Update scripts/assemble.sh to use jibDockerBuild |
5 min |
| PR title validation fails | No release type found in pull request title |
Use conventional commits format: chore: migrate to Java 25 |
2 min |
| Integration tests show NO-SOURCE | Skipping task ':integrationTest' as it has no source files |
Add explicit testClassesDirs and classpath in testing.gradle |
15 min |
| Plugin | Before | After | Notes |
|---|---|---|---|
| Gradle | 8.10.2 | 9.3.1 | Required for Java 25 |
| Lombok | 1.18.30 | 1.18.40 | Java 25 support |
| JaCoCo | 0.8.9 | 0.8.14 | Java 25 support |
| Avro plugin | commercehub 0.22.0 | davidmc24 1.9.1 | Gradle 9 support |
| Docker | palantir 0.36.0 | jib 3.5.2 | Replaced unmaintained plugin |
| docker-compose | 0.17.12 | 0.17.20 | Gradle 9 support |
| internal/org-specific plugin | (varies) | removed | Not compatible with Gradle 9 |
Gradle 9.x requires explicit configuration for custom Test tasks. Without this, integration tests silently skip with NO-SOURCE.
task integrationTest(type: Test) {
// Gradle 9.x requires explicit test class directories and classpath configuration
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'integration-test'
}
// ... rest of configuration
}When replacing Palantir Docker with Jib, remember to update build scripts:
# scripts/assemble.sh - Before:
./gradlew docker --no-daemon -x check -x composeUp --info --gradle-user-home ${HOME}/.gradle
# scripts/assemble.sh - After:
./gradlew jibDockerBuild --no-daemon -x check -x composeUp --info --gradle-user-home ${HOME}/.gradlesdk install java 25.0.1-amzn
sdk use java 25.0.1-amzn
./gradlew clean build # SUCCESS - All 18 unit tests pass
./gradlew integrationTest # SUCCESS - All 19 integration tests pass (after fix)- Java 25 requires Gradle 9.x (major version 69)
- Jib is a good replacement for Palantir Docker - no Docker daemon needed
- CRITICAL: Update
scripts/assemble.shwhen replacing Docker plugin with Jib - CRITICAL: Add
testClassesDirsandclasspathto custom Test tasks for Gradle 9.x - Watch for malformed Avro schemas - old plugins may have hidden issues
- ByteBuddy experimental flag needed until official Java 25 support
- PR titles must follow conventional commits format (e.g.,
chore: migrate to Java 25)
Duration: ~1 hour
Status: SUCCESS (merged)
build.gradle: springBootVersion 3.4.5 -> 3.5.0build.gradle: springCloudVersion 2024.0.0 -> 2025.0.0application.yml: Hibernate dialectPostgreSQL95Dialect->PostgreSQLDialect
| Issue | Error Message | Solution | Time Spent |
|---|---|---|---|
| Hibernate dialect deprecated | Warning about deprecated dialect | Update to PostgreSQLDialect | 5 min |
- Spring Boot 3.5 is a smooth upgrade from 3.4
- Hibernate dialect should be updated before 4.0
Duration: ~3 hours (including data-parity validation)
Status: SUCCESS
build.gradle:- springBootVersion: 3.5.0 -> 4.0.2
- springCloudVersion: 2025.0.0 -> 2025.1.1 (NOT 2026.x as some docs suggest)
- spring-boot-starter-aop -> spring-boot-starter-aspectj (renamed)
- spring-boot-starter-batch -> spring-boot-starter-batch-jdbc (modularized)
- flyway-core -> spring-boot-starter-flyway (required for proper integration)
- Added: spring-boot-starter-webmvc-test (for @WebMvcTest)
- Removed explicit JUnit version pins (managed by Spring Boot)
DataSourceConfig.java: Package relocation for DataSourcePropertiesNamedParameterJdbcCursorItemReader.java: Complete rewrite from class to factory patternEtlPipeline.java,JobDataController.java: Updated Spring Batch imports- All integration tests: @MockBean -> @MockitoBean, @WebMvcTest package change
| Issue | Error Message | Solution | Time Spent |
|---|---|---|---|
| spring-boot-starter-aop not found | Dependency resolution failure | Renamed to spring-boot-starter-aspectj | 5 min |
| DataSourceProperties not found | Package does not exist | Moved to o.s.boot.jdbc.autoconfigure | 5 min |
| Spring Batch packages not found | ExecutionContext, JdbcCursorItemReader | Moved to o.s.batch.infrastructure.* | 10 min |
| JdbcCursorItemReader setters removed | Spring Batch 6.0 uses builder-only pattern | Refactor to factory class with builder | 30 min |
| @WebMvcTest not found | Package relocation | o.s.boot.webmvc.test.autoconfigure | 5 min |
| @MockBean not found | Replaced by @MockitoBean | o.s.test.context.bean.override.mockito | 10 min |
| JUnit NoSuchMethodError | Version conflict with Spring Boot managed versions | Remove explicit version pins | 15 min |
| WebMvcTest dependency missing | Class not found at runtime | Add spring-boot-starter-webmvc-test | 5 min |
| Flyway not running at startup | Migrations skipped | Use spring-boot-starter-flyway instead of flyway-core | 10 min |
Spring Batch 6.0 removes setters from item readers. You cannot extend JdbcCursorItemReader and override methods. Must use builders:
// BEFORE (Spring Batch 5.x) - BROKEN in 6.0
public class NamedParameterJdbcCursorItemReader<T> extends JdbcCursorItemReader<T> {
@Override
public void setSql(String sql) {
super.setSql(NamedParameterUtils.substituteNamedParameters(sql, params));
}
}
// AFTER (Spring Batch 6.0) - Factory pattern with builder
public final class NamedParameterJdbcCursorItemReader {
public static <T> JdbcCursorItemReader<T> create(
DataSource dataSource, String sql,
SqlParameterSource params, RowMapper<T> rowMapper) {
String convertedSql = NamedParameterUtils.substituteNamedParameters(sql, params);
return new JdbcCursorItemReaderBuilder<T>()
.dataSource(dataSource)
.sql(convertedSql)
.rowMapper(rowMapper)
.preparedStatementSetter(createPss(sql, params))
.saveState(false)
.build();
}
}The migration guide incorrectly states Spring Cloud 2026.x - use 2025.1.x instead:
springBootVersion = '4.0.2'
springCloudVersion = '2025.1.1' // NOT 2026.x| Old (Spring Boot 3.x) | New (Spring Boot 4.0) |
|---|---|
@MockBean |
@MockitoBean |
o.s.boot.test.autoconfigure.web.servlet.WebMvcTest |
o.s.boot.webmvc.test.autoconfigure.WebMvcTest |
o.s.boot.autoconfigure.jdbc.DataSourceProperties |
o.s.boot.jdbc.autoconfigure.DataSourceProperties |
o.s.batch.item.* |
o.s.batch.infrastructure.item.* |
Validated migration by comparing ETL exports between Spring Boot 3.5 and 4.0.2:
- All 6 topics produced identical output (20,376 records total)
- Comparison requires resetting Kafka consumer offsets before each branch export
./gradlew clean build # SUCCESS
./gradlew test # SUCCESS - 18 unit tests
./gradlew integrationTest # SUCCESS - 19 integration tests
./gradlew <yourDataExportTask> -PexportName=springboot4 -Pstart="2024-01-15T00:00:00Z" -Pduration=60- Spring Boot 4.0 has extensive package relocations - check imports carefully
- Spring Batch 6.0 is a major breaking change - all item readers use builder pattern
- Use Spring Cloud 2025.1.x with Spring Boot 4.0 (not 2026.x)
- Test annotations moved to new packages - search/replace needed
- Flyway requires starter dependency for proper auto-configuration
- Modularized starters (batch-jdbc, webmvc-test) need explicit dependencies
| Repo | Java Migration | SpringBoot Migration | Total Time | Major Issues |
|---|---|---|---|---|
| example-service | 17 → 21 ✅ → 25 ✅ | 3.4 → 3.5 ✅ → 4.0 ✅ | ~6.5 hours | 22+ (all resolved) |
| Issue | Repos Affected | Standard Solution |
|---|---|---|
| Gradle version incompatibility | example-service | 8.10.2 for Java 21, 9.3.1 for Java 25 |
| Lombok Java 25 incompatibility | example-service | Upgrade to 1.18.40+ |
| Palantir Docker plugin deprecated | example-service | Replace with Google Jib |
| ByteBuddy/Mockito Java 25 | example-service | Add -Dnet.bytebuddy.experimental=true |
| JaCoCo Java 25 support | example-service | Upgrade to 0.8.14+ |
| CI hardcoded Java version | example-service | Update workflow yaml files |
| Integration tests NO-SOURCE in Gradle 9 | example-service | Add explicit testClassesDirs and classpath |
| CI docker task not found | example-service | Update assemble.sh for Jib |
| PR title validation | example-service | Use conventional commits format |
| Spring Boot 4.0 @MockBean removed | example-service | Use @MockitoBean from new package |
| Spring Boot 4.0 package relocations | example-service | Update imports (see table above) |
| Spring Batch 6.0 builder-only pattern | example-service | Refactor to factory + builder pattern |
| Spring Cloud version confusion | example-service | Use 2025.1.x with Spring Boot 4.0 |
| Flyway not auto-configured | example-service | Use spring-boot-starter-flyway |
- Always check CI workflow files - Java versions are often hardcoded, not read from
.java-version - Gradle version matrix - Java 21 needs Gradle 8.x, Java 25 needs Gradle 9.x
- Plugin compatibility is the biggest challenge - Many Gradle plugins don't support Gradle 9
- Jib over Palantir - Jib is actively maintained and doesn't require Docker daemon
- Test frameworks need special attention - ByteBuddy/Mockito lag behind Java releases
- Stacked PRs require careful rebasing - Always keep the target version during conflict resolution
- Hidden bugs surface during migrations - Old plugins may mask schema/type errors
- Incremental upgrades are safer - 17 → 21 → 25 is better than direct 17 → 25
- Update ALL build scripts when changing plugins -
scripts/assemble.shmust matchbuild.gradle - Gradle 9.x changes Test task behavior - Custom Test tasks need explicit source/classpath config
- PR titles matter - CI may validate conventional commits format
- Spring Boot 4.0 has extensive package relocations - Plan for import changes across codebase
- Spring Batch 6.0 is a breaking change - Setter-based configuration replaced with builders
- Spring Cloud 2025.1.x for Spring Boot 4.0 - Documentation may incorrectly suggest 2026.x
- Modularized starters require explicit dependencies - batch-jdbc, webmvc-test, flyway
- Data comparison requires clean consumer state - reset Kafka offsets before each branch export