How to Migrate from Spring Boot 3.x to 4.0.0: A Practical Guide
Purpose
I migrated several Spring Boot applications from 3.x to 4.0.0 last month. This guide shows the exact steps I followed, the problems I encountered, and how I solved them.
Spring Boot 4.0.0 brings Jakarta EE 10 support, Java 21 baseline, and better observability. But it’s not just a version change. I found breaking changes in auto-configuration, dependency updates, and testing patterns that required careful handling.
This post walks through the complete migration process I used, from pre-migration assessment to production deployment.
Environment Context
Before starting, I verified my environment:
- Current version: Spring Boot 3.2.x
- Target version: Spring Boot 4.0.0
- Java version: Java 17 → upgrading to Java 21
- Build tool: Maven 3.9+ (Gradle 8.x also works)
- Application type: REST API with JPA, Actuator, and security
Your setup might differ, but the core migration steps remain the same.
Pre-Migration Checklist
I learned the hard way that preparation matters. Before touching any dependencies, I ran these checks:
# Check current Spring Boot version./mvnw dependency:tree | grep spring-boot
# Identify custom auto-configurationsfind src -name "*AutoConfiguration*"
# Find deprecated property usagegrep -r "spring." src/ | grep -E "(deprecated|removed)"I created a dedicated migration branch:
git checkout -b feature/spring-boot-4.0-migrationThen I backed up critical files:
- application.properties and application.yml
- pom.xml (or build.gradle for Gradle users)
- Custom auto-configuration classes
- Any custom health indicators
This saved me when I needed to reference original configurations.
Step 1: Update Parent POM and Java Version
I started by updating the parent POM version in pom.xml:
Maven:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.0.0</version> <relativePath/></parent>
<properties> <java.version>21</java.version></properties>Gradle:
plugins { id 'org.springframework.boot' version '4.0.0' id 'io.spring.dependency-management' version '1.1.6'}
java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21}Alternative: Dependency Management Without Parent POM
If you can’t use the parent POM, import dependency management:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>4.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>I also updated my CI/CD pipeline to use Java 21:
# GitHub Actions example- uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin'Step 2: Add Property Migrator
This was the most helpful tool I found. I added the property migrator temporarily:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope></dependency>The migrator does two things:
- Prints diagnostics for renamed/removed properties at startup
- Temporarily maps old properties to new ones
When I started the application, it showed warnings like:
WARNING : The property 'spring.old.property' is deprecated. Use 'spring.new.property' instead.I went through each warning and updated my configuration files. Once all warnings were resolved, I removed the migrator dependency.
Step 3: Update Dependencies
After changing the parent POM, most starter dependencies updated automatically. But I verified these critical dependencies:
No version numbers needed - the parent POM manages them:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>For third-party libraries, I added the Maven enforcer plugin to catch version conflicts:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <executions> <execution> <id>enforce-versions</id> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <dependencyConvergence/> </rules> </configuration> </execution> </executions></plugin>This revealed conflicts I had to resolve with exclusions:
<dependency> <groupId>com.example</groupId> <artifactId>library</artifactId> <exclusions> <exclusion> <groupId>org.old.dependency</groupId> <artifactId>old-lib</artifactId> </exclusion> </exclusions></dependency>Step 4: Migrate Test Code
This is where I hit the most issues. Spring Boot 4.0.0 fully deprecated JUnit 4, so I had to update tests.
OutputCapture migration:
Old (JUnit 4):
import org.junit.Rule;import org.springframework.boot.test.system.OutputCaptureRule;
public class MyTest { @Rule public OutputCaptureRule outputCapture = new OutputCaptureRule();
@Test public void testOutput() { System.out.println("test"); assertThat(outputCapture.toString()).contains("test"); }}New (JUnit 5):
import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.springframework.boot.test.system.OutputCaptureExtension;import org.springframework.boot.test.system.CapturedOutput;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(OutputCaptureExtension.class)class MyTest {
@Test void testOutput(CapturedOutput output) { System.out.println("test"); assertThat(output).contains("test"); }}I also adopted the new MockMvcTester pattern for cleaner assertions:
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;import org.springframework.test.context.bean.override.mockito.MockitoBean;import org.springframework.test.web.servlet.assertj.MockMvcTester;
import static org.assertj.core.api.Assertions.assertThat;import static org.mockito.BDDMockito.given;
@WebMvcTest(UserVehicleController.class)class MyControllerTests {
@Autowired private MockMvcTester mvc;
@MockitoBean private UserVehicleService userVehicleService;
@Test void testExample() { given(this.userVehicleService.getVehicleDetails("sboot")) .willReturn(new VehicleDetails("Honda", "Civic"));
assertThat(this.mvc.get().uri("/sboot/vehicle") .accept(MediaType.TEXT_PLAIN)) .hasStatusOk() .hasBodyTextEqualTo("Honda Civic"); }}The AssertJ-style assertions are more readable and type-safe.
Step 5: Configuration Changes
I updated several configuration properties based on the migrator output:
Observability:
# RestClient observation enabled by default in 4.0.0management.observation.rest-client.enabled=true
# Health indicatorsmanagement.health.defaults.enabled=trueDatabase:
# Verify connection pool settings still workspring.datasource.hikari.maximum-pool-size=10
# JPA properties - mostly unchangedspring.jpa.hibernate.ddl-auto=nonespring.jpa.show-sql=falseTo review auto-configuration changes, I enabled debug mode temporarily:
debug: true
# Or specific for auto-configlogging.level.org.springframework.boot.autoconfigure=DEBUGThe startup log showed which auto-configurations matched and which didn’t. I found that:
- RestClient is now auto-configured with observation
- Health indicators restructured (non-sealed interface)
- Task executor builders updated
Step 6: Code Changes - RestClient Observation
One improvement I leveraged was RestClient auto-instrumentation. In Spring Boot 3.x, I had to manually instrument RestClient. In 4.0.0, it’s automatic:
@Servicepublic class ExternalApiService {
private final RestClient restClient;
public ExternalApiService(RestClient.Builder builder) { this.restClient = builder .baseUrl("https://api.example.com") .build(); }
public ExternalData fetchData() { // Automatically observed with Micrometer return restClient.get() .uri("/data") .retrieve() .body(ExternalData.class); }}Now I get metrics automatically:
http.client.requests(timer)- Outgoing request tracking
- Response status codes
- Exception tracking
No manual instrumentation needed.
Step 7: Testing Strategy
I followed a layered testing approach:
1. Unit Tests
./mvnw clean test./mvnw clean test jacoco:report # with coverage2. Integration Tests I verified database migrations:
./mvnw flyway:migrate # or liquibase:update3. Smoke Tests I created a basic smoke test class:
@SpringBootTest@AutoConfigureMockMvcclass SmokeTests {
@Autowired private MockMvc mvc;
@Test void applicationContextLoads() { // Basic sanity check }
@Test void actuatorHealthEndpointResponds() throws Exception { mvc.perform(get("/actuator/health")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("UP")); }}4. Performance Baseline Before migration, I recorded:
- Startup time
- Response times for critical endpoints
- Memory usage
- Throughput
After migration, I compared and found no degradation. Java 21 actually improved performance in some cases.
Common Issues and Solutions
Here are the problems I encountered and how I fixed them:
Issue 1: Dependency Convergence Errors
Error:
[WARNING] Rule 0: org.apache.maven.plugins.enforcer.DependencyConvergenceRule failedSolution:
# Find the conflict./mvnw dependency:tree -Dverbose
# Exclude the transitive dependencyIssue 2: NoSuchBeanDefinitionException
Error:
No qualifying bean of type 'com.example.Repository' availableSolution:
- Check auto-configuration report (
debug: true) - Enable DEBUG logging for auto-config
- Verify component scanning paths
Issue 3: @MockBean Not Working
Error:
Cannot use @MockBean on final classesSolution:
Switch to the new @MockitoBean annotation:
// Old@MockBeanprivate MyService service;
// New (4.0.0)@MockitoBeanprivate MyService service;Issue 4: Deprecated Property Warnings
Error:
The property 'spring.old.property' is deprecatedSolution:
- Let the property migrator show you the new property name
- Update application.properties
- Remove the migrator dependency after fixing all warnings
Post-Migration Checklist
Before considering the migration complete, I verified:
- All tests pass (unit, integration, e2e)
- Build succeeds without warnings
- Application starts successfully
- Actuator endpoints respond correctly
- No deprecated property warnings in logs
- Health checks pass
- Performance benchmarks acceptable
- Security scan passes
I also updated documentation:
- README with Spring Boot 4.0.0
- CI/CD pipeline configurations
- Deployment documentation
Finally, I removed the property migrator:
<!-- Remove after migration complete --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope></dependency>Deployment Strategy
I used a blue-green deployment approach:
- Deploy 4.0.0 to green environment
- Run smoke tests against green
- Switch traffic gradually (10%, 50%, 100%)
- Monitor metrics and error rates
- Rollback if issues detected
For database changes, I versioned migrations:
-- V4.0.0__add_new_column.sqlALTER TABLE users ADD COLUMN preferences TEXT;With rollback scripts ready:
-- U4.0.0__remove_new_column.sqlALTER TABLE users DROP COLUMN preferences;Summary
Migrating to Spring Boot 4.0.0 isn’t trivial, but it’s manageable with a systematic approach:
- Use the property migrator - it saves hours of debugging
- Update to Java 21 first - get that out of the way
- Migrate tests to JUnit 5 and MockMvcTester - cleaner, more maintainable
- Test at each layer - unit, integration, smoke, performance
- Deploy with blue-green strategy - safe rollback path
The key improvements I gained:
- Java 21 performance benefits
- Better observability with RestClient auto-instrumentation
- Cleaner test code with MockMvcTester
- Future-proof with Jakarta EE 10
Start with non-production environments, monitor closely, and plan your production rollout during a low-traffic window.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring Boot 4.0.0 Migration Guide
- 👨💻 Spring Boot 4.0.0 Release Notes
- 👨💻 Jakarta EE 10 Specification
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments