Spring Boot 4.0 migration looks straightforward on paper.
Update dependencies.
Fix a few warnings.
Run the app.
In reality? Things break. Quietly. Repeatedly. And sometimes in places you don’t expect.
We recently migrated a real production application from Spring Boot 3.x to 4.0, and I want to share what actually broke, why it broke, and how we fixed it, so you don’t lose days chasing cryptic errors.
Why Spring Boot 4.0 migrations feel deceptively hard
Spring Boot 4.0 isn’t just a version bump. It compounds changes from:
- Jakarta EE namespace enforcement
- Spring Framework 7
- Java baseline changes
- Security and observability refactors
Most issues don’t fail at compile time. They fail:
- at startup
- at runtime
- or worse, in tests only
1️⃣ Jakarta EE edge cases you thought you already fixed
Symptom
App compiles, but crashes on startup with ClassNotFoundException or NoSuchMethodError.
Even if you migrated to Jakarta in Spring Boot 3.x, some transitive dependencies still pull javax.* classes.
Common offenders:
- old validation libraries
- SOAP / JAXB tooling
- legacy servlet filters
Fix
Run:
mvn dependency:tree | grep javax
Then:
- force Jakarta-compatible versions
- exclude old transitive deps explicitly
👉 Lesson: “It worked in 3.x” doesn’t mean it’s safe in 4.0.
2️⃣ Spring Security config stopped compiling (or worse: silently changed behavior)
Symptom
Security config compiles, but authentication behaves differently.
Spring Security in the Spring Boot 4 era expects fully explicit configuration.
What broke:
- deprecated DSLs removed
- defaults changed (especially CSRF & session handling)
- custom filters no longer auto-registered
Fix
Move to explicit SecurityFilterChain beans and stop relying on defaults.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
👉 Lesson: implicit security configs are gone, be explicit or be surprised.
3️⃣ Starters that used to “just work” disappeared
Symptom
Application fails to start with missing beans.
Some Spring Boot starters were:
- renamed
- split
- or removed entirely
Especially around:
- observability
- actuator extensions
- legacy integrations
Fix
Audit your starters manually. Don’t trust the old list.
mvn dependency:tree | grep spring-boot-starter
Replace removed starters with:
- explicit libraries
- or new modular alternatives
4️⃣ Tests broke because Java versions quietly changed
Symptom
Tests fail locally but pass in CI (or the opposite).
Spring Boot 4.0 pushes modern Java harder (Java 21+).
What broke:
- Testcontainers images
- ByteBuddy / Mockito compatibility
- JVM flags removed or ignored
Fix
- Align local Java version, CI Java version, and Maven toolchains
- Upgrade testing libraries aggressively
👉 Lesson: don’t debug Spring errors when it’s really a JVM mismatch.
5️⃣ Actuator & metrics behavior changed without errors
Symptom
No errors — but metrics disappeared.
Spring Boot 4 refactors observability defaults:
- endpoints disabled by default
- renamed metrics
- different exposure rules
Fix
Re-declare actuator exposure explicitly:
management:
endpoints:
web:
exposure:
include: health,info,metrics
👉 Lesson: “no error” ≠ “working”.
When we realized this migration needed a checklist
After fixing the third breaking issue, we stopped and realized:
This isn’t a single migration — it’s a pattern.
So we documented every breaking change, every fix, and every gotcha into a structured migration checklist.
👉 Full Spring Boot 3.x → 4.0 migration guide
🔗 https://copilothub.directory/instructions/spring-boot-3x-to-40-migration-guide
It includes:
- dependency audit steps
- security migration patterns
- test & CI fixes
- actuator & observability changes
Bonus: where we’re collecting more migration playbooks
We’re building a small directory of real-world migration guides (not marketing docs):
If you’re dealing with:
- Spring Boot upgrades
- Java version jumps
- framework migrations
You’ll probably find something useful there.
What was the weirdest thing that broke during your Spring Boot upgrade?
Silent behavior change?
Dependency from 2015?
Security config surprise?
Let’s compare notes 👇

Top comments (0)