Spring Boot 4 was released in November 2025. It’s a major upgrade. Here’s exactly what will break in your application, and the precise steps to fix each issue.
This is Article 3in the Spring Boot Essentials 2026 series. Article 1 covers everything that changed from Spring Framework 6 to Spring Framework 7 and what it means for your codebase and Article 3 covers
Let’s be direct about what kind of article this is.
This is not a list of exciting new features. It’s not a “what’s new in Spring Boot 4” post. There are plenty of those.
This is the article you need when you’re staring at a build that was working fine on Spring Boot 3, and now it isn’t. Or when your tech lead just announced that the team is migrating, and you need to know exactly what’s going to break before you write a single line of migration code.
Spring Boot 4 built on Spring Framework 7 and Jakarta EE 11, is the most disruptive Spring release since the javax → jakarta namespace change in Spring 6. It raised the Java baseline, dropped Undertow, ripped out JUnit 4, upgraded Jackson to a breaking major version, and changed Spring Security defaults that silently break REST APIs.
Most of these breaks produce no obvious error message. They just make your application behave wrong.
This guide covers each breaking category with: what broke, why it broke, and the exact code to fix it.
Before You Touch Your POM: The Migration Strategy
One rule applies above everything else:
Never jump directly from Spring Boot 2.x or 3.x to Spring Boot 4 in a single commit.
The migration path is a two-phase staircase:

Why this matters: Every API deprecated in the 3.x lifecycle is removed in 4.0, not soft-deprecated, not conditionally available. Gone. If you skip the intermediate step, you’re fighting removal errors and unfamiliar APIs at the same time. That’s how migrations take months instead of weeks.
The process on the 3.5.x intermediate step:
- Upgrade to Spring Boot 3.5.x (latest patch)
- Add the properties migrator, it will list every property that has changed
- Run your test suite and treat every deprecation warning as a migration task, not a warning
- Clear all deprecations completely before incrementing to 4.0
Only then open the POM and change 3.5.x to 4.0.0.
Break #1: Java 21 Is Now the Minimum
What breaks: Your CI/CD pipeline, your Docker base images, and your local dev setup, if any of them are running Java 17.
Spring Boot 4 requires Java 21 as the minimum baseline. The primary driver is Jakarta EE 11, which draws on APIs that require Java 21. Virtual Threads, which Spring Boot 4 has first-class support for, are also a Java 21 feature.
How to fix it:



If you’re still on Java 17 infrastructure: Spring Boot 4 will technically run, because the Java baseline is 21, not 25. But you won’t have access to Virtual Threads, Scoped Values, or any of the Project Leyden startup optimizations. And Jakarta EE 11 features depend on Java 21 APIs. Upgrade to at least Java 21 before migrating to Spring Boot 4. Java 25 is the recommended production target in 2026.
Break #2: Undertow Is Gone
What breaks: Any application using spring-boot-starter-undertow will fail to start. Not with a graceful error, with a hard ClassNotFoundException because the Undertow autoconfiguration module no longer exists.
Undertow was dropped because it does not support Servlet 6.1, which is required by Jakarta EE 11. Spring Boot 4 made no exceptions.
How to fix it:
Switch to Tomcat (the default) or Jetty before upgrading. Do this while you’re still on Spring Boot 3.5.x.


If you had Undertow-specific configuration properties (thread counts, buffer sizes), they don’t map 1:1 to Tomcat. Run the properties migrator (covered below) and audit your application.properties after switching.
Break #3: Spring Security: The Silent 403
This is the break that hurts the most, because it doesn’t crash your application. It just makes your API silently return 403 Forbidden for every POST, PUT, DELETE, and PATCH request.
What changed: Spring Security 7 (bundled with Spring Boot 4) changed CSRF protection defaults to be more aggressively enforced. REST APIs that were previously working without an explicit SecurityFilterChain bean, relying on Spring’s auto-configuration to be permissive, will now block all state-changing requests.
Additionally, WebSecurityConfigurerAdapter , deprecated in Spring Security 5.7, is completely removed in Spring Security 7. If you’re still extending it, your application won’t compile.
Fix 1: Remove WebSecurityConfigurerAdapter entirely

Note: authorizeRequests() → authorizeHttpRequests(). antMatchers() → requestMatchers(). Both old APIs are removed in Spring Security 7.
Fix 2: Handle CSRF for stateless REST APIs
If your API uses JWT or OAuth2 bearer tokens and is completely stateless (no session cookies), CSRF protection is not needed, and must be explicitly disabled:

Fix 3: Handle CSRF for session-based authentication (traditional web apps)
If your application uses session cookies for authentication (form login, traditional Spring MVC apps), you need CSRF, but it must be explicitly configured for your frontend to read it:

The rule: In Spring Security 7, there is no implicit behavior. Every security behavior must be explicitly declared. If something isn’t working, add logging:

Look for “FilterSecurityInterceptor” and “CSRF” entries. The 403 source is almost always visible there.
Break #4: Jackson 3: Custom Serializers Stop Working
What breaks: Any class that extends JsonSerializer<T>, JsonDeserializer<T>, or StdSerializer<T> with Jackson 2.x APIs. Any direct use of ObjectMapper constructors. Any class registered via @JsonComponent.
Spring Boot 4 ships with Jackson 3 as the default JSON library. Jackson 3 is a major version with API-level breaking changes, it’s not a drop-in replacement.
Break 1: ObjectMapper construction

Break 2: Custom serializers
// BEFORE — Jackson 2 custom serializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
@Component
public class MoneySerializer extends StdSerializer<Money> {
public MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeStartObject();
gen.writeNumberField("amount", value.getAmount());
gen.writeStringField("currency", value.getCurrency().getCode());
gen.writeEndObject();
}
}
// AFTER — Jackson 3 (same structure, but updated imports and module registration)
import tools.jackson.databind.ser.std.StdSerializer;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext; // renamed from SerializerProvider
@Component
public class MoneySerializer extends StdSerializer<Money> {
public MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money value, JsonGenerator gen, SerializationContext ctxt)
throws JacksonException { // IOException → JacksonException
gen.writeStartObject();
gen.writeNumberField("amount", value.getAmount());
gen.writeStringField("currency", value.getCurrency().getCode());
gen.writeEndObject();
}
}
Key Jackson 3 API changes to know:

The search command to find everything you need to update:
# Find all files still using Jackson 2 package paths
grep -r "com.fasterxml.jackson" src/main/java --include="*.java" -l
grep -r "com.fasterxml.jackson" src/test/java --include="*.java" -l
If the output is long, prioritize: custom serializers/deserializers first, then ObjectMapper beans, then @JsonComponent classes.
Break #5: Hibernate 7 : The Database Layer
What breaks: Deprecated Hibernate Session methods that were removed, the renamed annotation processor dependency, and stricter temporal type handling.
Spring Boot 4 ships with Hibernate ORM 7.1 and Jakarta Persistence 3.2. Most JPQL queries survive unchanged, but specific Hibernate Session API usage will fail at compile time.
Break 1: Removed Session methods
Hibernate 7 removed the legacy Session API methods that were deprecated in Hibernate 6. These were the non-JPA-standard methods that Hibernate added decades ago:
// BEFORE — Hibernate 6 / Spring Boot 3 (deprecated but worked)
@Repository
public class UserRepository {
@PersistenceContext
private EntityManager em;
public void saveUser(User user) {
Session session = em.unwrap(Session.class);
session.save(user); // REMOVED in Hibernate 7
session.update(user); // REMOVED in Hibernate 7
session.saveOrUpdate(user); // REMOVED in Hibernate 7
session.delete(user); // REMOVED in Hibernate 7
}
}
// AFTER — Hibernate 7 / Spring Boot 4 (JPA standard)
@Repository
public class UserRepository {
@PersistenceContext
private EntityManager em;
public void saveUser(User user) {
em.persist(user); // was: save()
em.merge(user); // was: update() / saveOrUpdate()
em.remove(user); // was: delete()
}
}
Break 2: Annotation processor dependency renamed
If you use the JPA Static Metamodel Generator (for type-safe Criteria API queries), the artifact ID changed:
<!-- BEFORE — Spring Boot 3 / Hibernate 6 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<scope>provided</scope>
</dependency>
<!-- AFTER — Spring Boot 4 / Hibernate 7 -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-processor</artifactId>
<scope>provided</scope>
</dependency>
Break 3: Stricter temporal type handling
Hibernate 7 enforces stricter alignment between Java temporal types and database column types. Instant and OffsetDateTime fields that previously worked without explicit column definitions may now throw schema validation errors.
// BEFORE — Spring Boot 3, this worked without column type specification
@Entity
public class AuditLog {
@Column
private Instant createdAt; // could silently map to various DB types
}
// AFTER — Spring Boot 4, be explicit about temporal mapping
@Entity
public class AuditLog {
@Column(columnDefinition = "TIMESTAMP WITH TIME ZONE")
private Instant createdAt;
// Or use the JPA 3.2 standard annotation
@Temporal(TemporalType.TIMESTAMP)
@Column
private Instant createdAt;
}
How to catch schema issues before production:
Add this to your staging/test profile to run schema validation at startup:
# application-staging.yml
spring:
jpa:
hibernate:
ddl-auto: validate # Fail fast if schema doesn't match entities
This turns potential silent data corruption issues into hard startup failures, which is exactly what you want during migration testing.
Break #6: JUnit 4 Is Gone
What breaks: Every test class using @RunWith, @Rule, @ClassRule, JUnit 4 Assert, or any JUnit 4 runner. This includes Spring’s own @RunWith(SpringRunner.class).
Spring Boot 4 removed junit-vintage-engine , the bridge that allowed JUnit 4 tests to run inside the JUnit 5 platform. If you have any JUnit 4 tests, they will simply not run, no error, no output, they are invisible to the test runner.
The migration pattern:
// BEFORE — JUnit 4
import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class) // JUnit 4 runner
@SpringBootTest
public class UserServiceTest {
@Before
public void setUp() {
// setup
}
@Test
public void testCreateUser() {
User user = userService.create("alice");
assertNotNull(user); // JUnit 4 Assert
assertEquals("alice", user.name());
}
}
// AFTER — JUnit 5 (Jupiter)
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest // No @RunWith needed — JUnit 5 uses @ExtendWith internally
public class UserServiceTest {
@BeforeEach // was @Before
public void setUp() {
// setup
}
@Test
public void testCreateUser() {
User user = userService.create("alice");
assertNotNull(user); // same method, different import
assertEquals("alice", user.name());
}
}
Full JUnit 4 → JUnit 5 annotation mapping:

The command to find all JUnit 4 tests:
grep -r "org.junit.Test|org.junit.Before|RunWith|SpringRunner"
src/test/java --include="*.java" -l
Every file in that output needs migration.
Break #7: Configuration Properties: Use the Migrator
Spring Boot 4 renames and removes dozens of configuration properties. Hunting these manually is error-prone. Use the official tool:
Step 1: Add the properties migrator as a runtime dependency (temporarily)
<!-- pom.xml — add this during migration only -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
// build.gradle
runtimeOnly("org.springframework.boot:spring-boot-properties-migrator")
Step 2: Start your application and read the output
The migrator prints a startup report like this:
WARN : spring-boot-properties-migrator - The following properties are deprecated:
spring.redis.host -> spring.data.redis.host
spring.datasource.hikari.maxLifetime -> spring.datasource.hikari.max-lifetime
management.metrics.export.prometheus.enabled -> management.prometheus.metrics.export.enabled
server.undertow.threads.io -> [REMOVED — Undertow is not available]
Please refer to the migration guide for instructions on how to update your configuration.
Step 3: Update your application.properties / application.yml
Fix every property the migrator identified.
Step 4: Remove the migrator dependency
<!-- Remove this before committing the migration -->
<!-- spring-boot-properties-migrator should NOT be in production -->
Leaving the migrator in production adds startup overhead and may hide future property issues.
Break #8: Null Safety: JSpecify Replaces Spring Annotations
What breaks: Kotlin projects in particular. Any code using org.springframework.lang.@Nullable or org.springframework.lang.@NonNull to interact with Spring APIs.
Spring Boot 4 adopted JSpecify (org.jspecify.annotations) as the standard for null-safety annotations across the entire Spring Framework codebase. Spring’s own @Nullable and @NonNull from org.springframework.lang are removed from the framework’s public API surface.
How to fix it:
<!-- Add JSpecify dependency -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
// BEFORE — Spring 6 / Spring Boot 3
import org.springframework.lang.Nullable;
import org.springframework.lang.NonNull;
public interface UserRepository {
@Nullable
User findByEmail(@NonNull String email);
}
// AFTER — Spring Boot 4
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NonNull;
public interface UserRepository {
@Nullable
User findByEmail(@NonNull String email);
}
The annotation names are identical, only the import package changes. In IntelliJ IDEA, a project-wide find-and-replace on the import path handles this in under a minute:
Find: import org.springframework.lang.Nullable;
Replace: import org.jspecify.annotations.Nullable;
Find: import org.springframework.lang.NonNull;
Replace: import org.jspecify.annotations.NonNull;
For Kotlin projects, this change is more impactful. JSpecify is recognized by the Kotlin compiler (K2 and later), which means previously-suppressed null-safety warnings may now surface as compilation errors. Treat them as real issues to fix, not noise to suppress.
The Full Migration Checklist
Use this as your team’s working checklist. Work through it sequentially, earlier items unblock later ones.
Phase 1: Pre-migration (on Spring Boot 3.5.x)
- [ ] Upgrade to Spring Boot 3.5.x (latest patch)
- [ ] Add spring-boot-properties-migrator and fix all property warnings
- [ ] Fix all deprecation warnings in your IDE (treat warnings as errors during this phase)
- [ ] If using Undertow: switch to Tomcat or Jetty
- [ ] Migrate WebSecurityConfigurerAdapter → SecurityFilterChain
- [ ] Migrate authorizeRequests() → authorizeHttpRequests()
- [ ] Migrate antMatchers() → requestMatchers()
- [ ] Migrate all JUnit 4 tests → JUnit 5
- [ ] Replace hibernate-jpamodelgen with hibernate-processor in the POM
- [ ] Replace session.save(), session.update(), session.delete() with JPA standard calls
- [ ] Remove spring-boot-properties-migrator before the version bump
Phase 2: The version bump
- [ ] Update java.version to 21 in POM/Gradle
- [ ] Update Docker base images to eclipse-temurin:21-jre (or 25-jre)
- [ ] Update CI/CD Java version
- [ ] Update Spring Boot version to 4.0.x
- [ ] Run mvn dependency:tree | grep javax , any remaining javax.* (not jakarta.*) is a third-party library problem
Phase 3: Post-bump fixes
- [ ] Migrate Jackson 2 custom serializers to Jackson 3 (tools.jackson.* packages)
- [ ] Update ObjectMapper construction to JsonMapper.builder().build()
- [ ] Update import org.springframework.lang.Nullable → import org.jspecify.annotations.Nullable
- [ ] Run tests with spring.jpa.hibernate.ddl-auto=validate on staging
- [ ] Audit explicit CSRF configuration in all SecurityFilterChain beans
- [ ] Verify all POST/PUT/DELETE endpoints respond correctly
The Migration That’s Worth It
Major version migrations are always painful. Spring Boot 4 is no exception.
But the specifics of each break are actually logical. Undertow couldn’t support Jakarta EE 11, it was never going to. Jackson 2 had reached the limits of its API design, Jackson 3’s JacksonException and builder pattern are cleaner. JUnit 4 has been superseded for years, there was never a good reason to keep carrying it. CSRF defaults that silently permitted unsafe configurations were a security risk, not a convenience.
Every change on this list is the Spring team cleaning up 10+ years of accumulated weight. The applications that come out the other side of this migration are leaner, more secure, and built on a stack that has a clear forward direction through Java 21, Jakarta EE 11, and the cloud-native infrastructure of the next five years.
The migration is the price of admission. The article you’re reading gives you the exact change to write in each room before you walk through the door.
Next in the Spring Boot Essentials 2026 series: Virtual Threads in Spring Boot 4: What Actually Changes for Your Code.
Beyond the Code: The Realities of Engineering
Running a major framework migration in a production codebase means more than knowing what to fix. It means convincing your team it’s worth doing, managing the risk conversation with your tech lead, and owning the outcome when something breaks in staging at 11pm.
The technical skills get you to the door. What happens on the other side is a different set of skills entirely.
I’ve distilled the unwritten rules of navigating software engineering careers into a practical field guide.
👉 Get “Beyond the Offer Letter” on Gumroad here: and learn how software companies actually work so you can accelerate your engineering career.
Spring Boot 4 Migration Guide: What Breaks and How to Fix It was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.
This post first appeared on Read More