Migrating from Spring Boot 2.x to 3.x: A Complete Checklist

Migrating from Spring Boot 2.x to 3.x: A Complete Checklist

Migrating from Spring Boot 2.x to 3.x: A Complete Checklist

Spring Boot 3.0 is a major milestone—the first version built on top of Spring Framework 6 and requiring Java 17 as the baseline. It brings Jakarta EE 9+, a revamped security model, native image support via GraalVM, and many performance improvements. But migrating a production application from 2.x to 3.x is not a trivial task. I’ve recently completed this migration for a mid-sized microservices system, and this post distills everything you need to know into a clear, actionable checklist.

Why Migrate?

Before diving into the how, let’s talk about the why. Spring Boot 2.x will reach its end of life in November 2023 (for 2.7) and November 2025 (for 2.6 LTS). Beyond security patches, you miss out on:

Prerequisites

Step 1: Update Your Build Configuration

Maven

Update your pom.xml to change the parent version and add the Spring Boot 3 BOM if needed:

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>

If you use a custom parent, add the BOM:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Also update the maven-compiler-plugin to target Java 17:

1
2
3
4
5
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>

Gradle

For Gradle, update your build.gradle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// ...
}

If you use Kotlin, ensure you’re on Kotlin 1.8+.

Step 2: Migrate from Java EE to Jakarta EE

This is the most impactful change. Spring Boot 3 replaces the javax.* namespace with jakarta.*. The migration involves:

How to Migrate

  1. Use a tool: The OpenRewrite project provides automated recipes. Add this to your pom.xml:
1
2
3
4
5
6
7
8
9
10
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.6.1</version>
<configuration>
<activeRecipes>
<recipe>org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0</recipe>
</activeRecipes>
</configuration>
</plugin>

Then run:

1
mvn rewrite:run
  1. Manual search-and-replace: If you prefer a hands-on approach, use your IDE’s global search and replace:

    • javax.persistencejakarta.persistence
    • javax.validationjakarta.validation
    • javax.servletjakarta.servlet
    • javax.annotationjakarta.annotation
  2. Check third-party dependencies: Libraries like Hibernate, Tomcat, and Jersey have Jakarta-compatible versions. Ensure you’re using the correct ones:

    • Hibernate 6.1+
    • Tomcat 10+
    • Thymeleaf 3.1+

Step 3: Update Spring Security Configuration

Spring Security 6 introduces a more declarative, component-based configuration. The old WebSecurityConfigurerAdapter is deprecated and removed.

Before (Spring Boot 2.x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password(passwordEncoder().encode("password")).roles("USER");
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

After (Spring Boot 3.x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}

@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

Key changes:

Step 4: Update Configuration Properties

Several properties have been renamed or removed. Use the Spring Boot 3 migration guide or run your app with --debug to see warnings.

Common Changes

Old Property (2.x) New Property (3.x)
server.servlet.session.timeout server.servlet.session.timeout (unchanged, but now uses Duration)
spring.datasource.hikari.connection-timeout spring.datasource.hikari.connection-timeout (now uses Duration)
spring.jpa.hibernate.ddl-auto spring.jpa.hibernate.ddl-auto (unchanged)
spring.mvc.servlet.load-on-startup Removed, use @Order on WebMvcConfigurer
spring.flyway.enabled spring.flyway.enabled (unchanged)
management.metrics.export.prometheus.enabled management.prometheus.metrics.export.enabled

Duration Format

Spring Boot 3 now expects durations in the ISO-8601 format (e.g., PT30S for 30 seconds) or with a suffix (30s). Update your application.yml:

1
2
3
4
5
6
7
8
9
server:
servlet:
session:
timeout: 30m # or PT30M

spring:
datasource:
hikari:
connection-timeout: 10s

Step 5: Update Spring Data and JPA

Spring Data 2022.0+ aligns with Jakarta EE. The main changes:

Hibernate 6

Hibernate 6 is the default. Key differences:

1
2
3
4
5
spring:
jpa:
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

Step 6: Update Actuator and Metrics

Spring Boot 3 uses Micrometer 1.10, which has a new Observation API for metrics and tracing.

Metrics

Health Indicators

Custom health indicators now implement HealthIndicator (same interface), but the response format changed. Ensure your custom health checks return proper statuses.

Tracing

Spring Boot 3 integrates with Micrometer Tracing. Replace Spring Cloud Sleuth dependencies:

1
2
3
4
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>

And configure:

1
2
3
4
management:
tracing:
sampling:
probability: 1.0

Step 7: Handle Deprecations and Removals

Spring Boot 3 removes many deprecated APIs from 2.x. Watch for:

Step 8: Test Thoroughly

Run your full test suite early and often. Common issues:

Sample Test Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
void testPublicEndpoint() throws Exception {
mockMvc.perform(get("/public/health"))
.andExpect(status().isOk());
}
}

Step 9: Check Third-Party Libraries

Update all dependencies to versions compatible with Spring Boot 3:

Step 10: Leverage New Features

Once migrated, take advantage of Spring Boot 3’s capabilities:

Complete Migration Checklist

Key Takeaways

Migrating to Spring Boot 3 is an investment that pays off with better performance, easier maintenance, and access to the latest Java features. Use this checklist as your roadmap, and you’ll be running on the latest and greatest in no time.