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

  1. Week 1: Fix all Boot 3 deprecation warnings — no version change
  2. Week 2: Bump to Boot 4 on a feature branch — fix compilation errors
  3. Week 3: Run full test suite — fix test failures
  4. Week 4: Deploy to staging — observe virtual threads, check pinning, monitor metrics
  5. 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 / mvcMatchersrequestMatchers; authorizeRequests()authorizeHttpRequests()
  • Hibernate 7 changes: use getReferenceById() instead of getById(), @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.