Migrating from Spring Boot 3.x to 4.0
Migrating from Spring Boot 3.x to 4.0 is straightforward if you’ve kept up with deprecation warnings. This guide walks through every step — from dependency updates to API changes — with before/after examples.
Pre-Migration: Fix Boot 3 Deprecations
Before bumping the version, fix all deprecation warnings in your current Boot 3.x project. Every deprecated API in Boot 3 is removed in Boot 4. In IntelliJ IDEA: Analyze → Code Cleanup with “Remove deprecated usages” enabled.
# Also check for compile warnings
./mvnw clean package -Werror 2>&1 | grep "deprecated"
Common Boot 3 deprecations to fix first:
// ❌ Boot 3 deprecated → Boot 4 removed
health.up().withDetail("key", value).build()
// ✓ No change needed — still valid
// ❌ Deprecated MockMvc usage
mockMvc.perform(get("/").with(csrf()))
// ✓ Still valid — no change
// ❌ Security deprecated in 3.x
http.csrf().disable()
// ✓ Fix before migrating:
http.csrf(AbstractHttpConfigurer::disable)
Step 1: Update Parent POM
<!-- Before -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
</parent>
<!-- After -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
</parent>
Step 2: Update Java Version
Spring Boot 4 requires Java 17+. Java 21 is recommended:
<properties>
<java.version>21</java.version>
</properties>
Step 3: Update Spring Cloud (if used)
Spring Cloud has its own release train. Boot 4 requires the 2025.x release train:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Step 4: Build and Check Compilation Errors
./mvnw clean compile
Most compilation errors will be removed APIs. The errors tell you exactly where to fix.
Common Compilation Errors and Fixes
Spring Security
// ❌ Removed — RequestMatcher methods changed
http.antMatcher("/api/**")
http.mvcMatchers("/api/**")
// ✓ Fix:
http.securityMatcher("/api/**")
http.requestMatchers("/api/**")
// ❌ Removed
http.authorizeRequests()
// ✓ Fix:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated())
// ❌ Removed — use requestMatchers overloads
.antMatchers("/api/**").hasRole("USER")
// ✓ Fix:
.requestMatchers("/api/**").hasRole("USER")
Spring MVC
// ❌ Removed — ResponseEntityExceptionHandler method signature changed
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, // ← HttpStatus removed
WebRequest request) { }
// ✓ Fix: use HttpStatusCode
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status, // ← HttpStatusCode
WebRequest request) { }
Spring Data
// ❌ Removed in Spring Data 4
repository.getById(id) // threw EntityNotFoundException
repository.getOne(id) // deprecated in Spring Data 3
// ✓ Fix:
repository.getReferenceById(id) // returns proxy, throws on access
repository.findById(id).orElseThrow() // loads immediately
Hibernate 7 Changes
// ❌ Hibernate 7 removes some deprecated APIs
// Session.load() → use Session.getReference()
// Criteria (old API) → use JPA Criteria API
// @Type is more restricted in Hibernate 7
@Column(columnDefinition = "jsonb")
@Type(JsonType.class) // check Hibernate Types library compatibility
private Map<String, Object> metadata;
// ✓ Prefer @JdbcTypeCode for standard types:
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> metadata;
Step 5: Fix Property Changes
Some application.yml properties changed:
# ❌ Boot 3 — removed in Boot 4
spring:
mvc:
pathmatch:
use-suffix-pattern: true # removed
# ❌ Boot 3 health details config
management:
endpoint:
health:
show-details: always
# ✓ Boot 4 — more granular
management:
endpoint:
health:
show-details: when-authorized
roles: ACTUATOR_ADMIN # new
# ❌ Logging pattern tokens changed in Logback 1.5
%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} # syntax slightly changed
# Check Logback 1.5 migration notes if using custom patterns
Run this to find all deprecated or removed properties:
./mvnw spring-boot:run 2>&1 | grep "deprecated\|removed\|replaced"
Spring Boot logs warnings for deprecated properties on startup.
Step 6: Test Changes
// MockMvc — largely unchanged, but verify imports
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
// Spring Security Test — unchanged
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
// Testcontainers — update to 1.20.x if not already
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
// No changes needed — @ServiceConnection API unchanged
Step 7: Virtual Threads Compatibility
Virtual threads are on by default in Boot 4. Test for pinning issues:
# Enable pinning detection (Java 21+)
JAVA_OPTS="-Djdk.tracePinnedThreads=full" ./mvnw spring-boot:run
Look for: java.lang.VirtualThread[...] pinned in the logs. Pinning happens when virtual threads use synchronized blocks with blocking I/O inside. The most common culprit is older JDBC drivers — upgrade to the latest PostgreSQL/MySQL JDBC driver.
<!-- Update JDBC drivers for virtual thread compatibility -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<!-- Boot 4 manages a compatible version -->
</dependency>
If you find pinning that you can’t fix, disable virtual threads:
spring:
threads:
virtual:
enabled: false
Step 8: Native Image (if applicable)
AOT processing improved in Boot 4 — fewer manual RuntimeHintsRegistrar entries needed. Re-run native tests after migration:
./mvnw test -PnativeTest
Some previously-needed hints may now be auto-detected. Remove redundant @ImportRuntimeHints if they cause issues.
Migration Timeline Recommendation
- Week 1: Fix all Boot 3 deprecation warnings — no version change
- Week 2: Bump to Boot 4 on a feature branch — fix compilation errors
- Week 3: Run full test suite — fix test failures
- Week 4: Deploy to staging — observe virtual threads, check pinning, monitor metrics
- Week 5: Production rollout — canary deploy, monitor error rates
Don’t rush step 4. Virtual threads and Hibernate 7 can surface bugs (LazyInitializationException, connection pool exhaustion) that only appear under real load.
What You’ve Learned
- Fix all Boot 3 deprecation warnings before bumping the version — everything deprecated is removed
antMatcher/mvcMatchers→requestMatchers;authorizeRequests()→authorizeHttpRequests()- Hibernate 7 changes: use
getReferenceById()instead ofgetById(),@JdbcTypeCode(JSON)for JSON columns - Virtual threads are on by default — test for pinning with
-Djdk.tracePinnedThreads=full - Spring Cloud requires the 2025.x release train with Boot 4
- Migrate incrementally: fix deprecations → compile → tests → staging → production
Next: Article 57 — Null Safety with JSpecify — leverage compile-time null safety in your Spring Boot applications.