Skip to content

Breaking Changes in Spring Boot 4.0.0 and How to Fix Them

Problem

Last week I upgraded our Spring Boot application from 3.2 to 4.0.0. The build passed, but the application failed to start. Configuration properties weren’t being loaded. Security filters were blocking all requests. Even the actuator health endpoint was returning 401 Unauthorized.

I hit these issues because Spring Boot 4.0.0 introduced major breaking changes. The minimum Java version is now 21. Spring Framework 7 replaced Spring Framework 6. Configuration properties changed. Security defaults shifted.

After spending two days fixing the upgrade, I learned the right way to approach this migration. The key is using the Spring Boot Properties Migrator tool before upgrading. It shows you exactly what will break and how to fix it.

In this post, I’ll show you the breaking changes I encountered and how I fixed them.

Environment Context

Here’s what I was running before the upgrade:

  • Spring Boot: 3.2.5
  • Java: 17
  • Spring Framework: 6.1.x
  • Spring Security: 6.2.x

The target after upgrade:

  • Spring Boot: 4.0.0
  • Java: 21
  • Spring Framework: 7.0.x
  • Spring Security: 6.4.x

What Happened: Common Breaking Changes

When I first ran the application after upgrading, I hit these issues immediately:

1. Java Version Requirement

The build failed with an error about Java version. Spring Boot 4.0.0 requires Java 21 as a minimum.

Before (pom.xml):

<properties>
<java.version>17</java.version>
</properties>

After (pom.xml):

<properties>
<java.version>21</java.version>
</properties>

This was the first thing I had to fix. I updated our CI/CD pipeline to use Java 21, updated my local development environment, and then the build passed.

2. Configuration Properties Not Loading

After the build passed, the application started but none of our custom configuration properties were loading. I saw warnings in the logs:

***************************
APPLICATION FAILED TO START
***************************
Description:
The following configuration properties are deprecated:
server.compression.enabled -> server.compression.enabled
spring.datasource.schema -> spring.datasource.schema
management.endpoints.web.exposure.include -> management.endpoints.web.exposure.include

The property names hadn’t changed, but some of the validation logic had. I needed to use the Properties Migrator tool to catch all of these issues before they became runtime problems.

3. Security Blocking All Requests

All HTTP requests were returning 401 Unauthorized. Even the actuator health endpoint was secured.

This happened because Spring Boot 4.0 changed the default security behavior. Web applications are now secured by default, and the default SecurityFilterChain configuration changed.

4. Dependency Version Conflicts

I got NoSuchMethodError from several third-party libraries. They were compiled against older versions of Spring Framework and weren’t compatible with Spring Framework 7.

I had to update several dependencies to their -jakarta variants and ensure all Spring dependencies were at version 7.x.

How to Solve: Use Properties Migrator

The most important tool I discovered was the Spring Boot Properties Migrator. This is a runtime dependency that analyzes your configuration and tells you exactly what will break.

Here’s how to use it:

Step 1: Add the Dependency BEFORE Upgrading

Add this to your project while still on Spring Boot 3.x:

Maven (pom.xml):

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>

Gradle (build.gradle):

runtimeOnly("org.springframework.boot:spring-boot-properties-migrator")

Step 2: Run Your Application

Start your application normally. The migrator will:

  • Print diagnostics about deprecated properties
  • Automatically migrate properties at runtime
  • Show you what needs to be changed in your configuration files

Step 3: Update Your Configuration

Fix each deprecated property the migrator identifies. Here are some common ones I encountered:

server.compression.enabled - unchanged, but validation logic changed

# Still valid in 4.0.0
server:
compression:
enabled: true

spring.datasource.schema - replaced by spring.sql.init.schema-locations

# Before (deprecated)
spring:
datasource:
schema: classpath:schema.sql
# After
spring:
sql:
init:
schema-locations: classpath:schema.sql

management.endpoints.web.exposure.include - unchanged but default values changed

# Before - actuator endpoints were open by default in some cases
management:
endpoints:
web:
exposure:
include: health,info,metrics
# After - explicitly configure what you need
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus

Step 4: Remove the Migrator

Once you’ve updated all your configuration properties and the application runs without warnings, remove the migrator dependency:

<!-- Remove this -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>

Specific Breaking Changes with Solutions

Jackson 2 to Jackson 3 Migration

Spring Boot 4.0 uses Jackson 3.x instead of Jackson 2.x. Most properties are deprecated:

Before:

spring:
jackson:
serialization:
write-dates-as-timestamps: false
default-property-inclusion: non_null

After:

spring:
jackson:
serialization:
write-dates-as-timestamps: false
default-property-inclusion: non_null

The property names haven’t changed, but the underlying implementation has. The Properties Migrator will warn you about any Jackson-specific properties that need attention.

Security Configuration Changes

The default security behavior changed in Spring Boot 4.0. Web applications are now secured by default with basic authentication.

Before (Spring Boot 3.x):

@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().permitAll()
);
return http.build();
}
}

This configuration now behaves differently. permitAll() on anyRequest() still permits requests, but the default is now authenticated().

After (Spring Boot 4.0):

@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatcher(EndpointRequest.toAnyEndpoint())
.hasRole("ACTUATOR_ADMIN")
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
}

For actuator endpoints, the security rules changed. You need to explicitly configure who can access them:

http
.authorizeHttpRequests((authorize) -> authorize
.requestMatcher(EndpointRequest.to("health")).permitAll()
.requestMatcher(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR_ADMIN")
.anyRequest().authenticated()
);

Jakarta EE Namespace Updates

Several dependencies now require -jakarta variants. I hit this with Infinispan:

Before:

<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
</dependency>

After:

<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core-jakarta</artifactId>
</dependency>

Check all your third-party dependencies and use the -jakarta variants where available.

Virtual Threads Configuration

Spring Boot 4.0 has improved support for virtual threads. If you want to use them:

application.yml:

spring:
threads:
virtual:
enabled: true

This requires Java 21+. For the best performance, use Java 24+ which has better virtual threads implementation.

Auto-Configuration Changes

Some auto-configuration classes changed their conditional registration. I had a custom DataSource auto-configuration that stopped working because the conditional property changed:

Before:

@Configuration
@ConditionalOnProperty(name = "custom.datasource.enabled", havingValue = "true")
public class CustomDataSourceAutoConfiguration {
// ...
}

This still works, but the default behavior changed. Now you need to explicitly disable the default DataSource auto-configuration if you want to use a custom one:

After:

@Configuration
@ConditionalOnProperty(name = "custom.datasource.enabled", havingValue = "true")
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
public class CustomDataSourceAutoConfiguration {
// ...
}

Before and After Code Examples

Here’s a complete example showing the key changes:

application.yml

Before (Spring Boot 3.2):

spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
schema: classpath:schema.sql
initialization-mode: always
jackson:
default-property-inclusion: non_null
serialization:
write-dates-as-timestamps: false
management:
endpoints:
web:
exposure:
include: health,info
server:
compression:
enabled: true

After (Spring Boot 4.0.0):

spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
sql:
init:
schema-locations: classpath:schema.sql
mode: always
jackson:
default-property-inclusion: non_null
serialization:
write-dates-as-timestamps: false
threads:
virtual:
enabled: true
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
server:
compression:
enabled: true

Security Configuration

Before (Spring Boot 3.2):

@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().permitAll()
);
return http.build();
}
}

After (Spring Boot 4.0.0):

@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatcher(EndpointRequest.to("health")).permitAll()
.requestMatcher(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR_ADMIN")
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
}

pom.xml

Before (Spring Boot 3.2):

<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.5</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
</dependency>
</dependencies>

After (Spring Boot 4.0.0):

<properties>
<java.version>21</java.version>
<spring-boot.version>4.0.0</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core-jakarta</artifactId>
</dependency>
<!-- Add this before upgrading to find deprecated properties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

Common Issues and Solutions

Issue: Properties Migrator Not Catching Changes

If the Properties Migrator doesn’t show warnings for properties in @PropertySource files, it’s because it only analyzes main configuration files.

Solution: Manually review all @PropertySource annotations and update the referenced files.

Issue: Security Filter Chain Conflicts

If you get an error about multiple SecurityFilterChain beans:

Parameter 0 of method securityFilterChain in ... required a single bean, but 2 were found

Solution: Remove custom SecurityFilterChain beans or consolidate them into one using @Order.

Issue: Test Failures with Security

Tests that passed before are now failing with 401 Unauthorized errors.

Solution: Add security annotations to test methods:

@Test
@WithMockUser(username = "admin", roles = "ADMIN")
public void testAdminEndpoint() {
// ...
}

Or use SecurityMockMvc:

@Test
public void testAdminEndpoint() {
mockMvc.perform(get("/admin")
.with(SecurityMockMvcConfigurers.mockUser("admin")))
.andExpect(status().isOk());
}

Summary

Spring Boot 4.0.0 introduced major breaking changes. The key points:

  1. Java 21 is required - Update your build and CI/CD pipelines first
  2. Use the Properties Migrator - Add it before upgrading to catch deprecated properties
  3. Security defaults changed - Review your SecurityFilterChain configuration
  4. Dependencies updated - Check for -jakarta variants and Spring Framework 7 compatibility
  5. Configuration properties changed - Update spring.datasource.* to spring.sql.init.*

The migration took me about two days for a mid-sized application. Most of that time was fixing configuration properties and security rules. Using the Properties Migrator tool saved hours of debugging.

If you’re planning to upgrade to Spring Boot 4.0.0, start by adding the Properties Migrator to your current 3.x project. Run it and fix all warnings before upgrading. This will save you a lot of headaches.

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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments