Migrating to Spring Boot 4: What 70 modules, Null-Safety and API versioning actually change
Spring Boot 4 and Framework 7 are the first baseline reset in years — Jakarta EE 11, a 70-module core, JSpecify null-safety, and built-in resilience. Here's the honest migration field guide, from the architecture, developer, and DevOps angles.
On this page
# The Problem
You inherited a Spring Boot 3.x service two years ago and it’s been fine. Then the security team files a ticket: a CVE in a transitive dependency, and the fix requires Spring Framework 7. You open the release notes to scope a “quick version bump” and the ground shifts under you. The core has been split into 70-odd modules. There’s a new nullability story. RestTemplate is on its way out. Jakarta EE 11 changed namespaces you thought you’d already migrated. Your “afternoon task” is now a project.
Here’s the reframe that makes it bearable: Spring Boot 4 is a genuine baseline reset — the first in years — and most of what looks like breakage is actually the framework absorbing glue code you’ve been maintaining by hand. Resilience4j, Feign, manual OpenTelemetry wiring: a lot of that is now built in. The migration is real work, but you come out the other side with less code to own, not more. You just have to read the map before you start walking.
# What Actually Changed
Spring Boot 4.0.0 shipped on 20 November 2025 alongside Spring Framework 7.0, with 4.1 following in June 2026. This isn’t a cosmetic bump. The headline changes:
- Jakarta EE 11 is fully adopted. The
javax.*→jakarta.*migration is now non-negotiable. - The core split into 70+ focused modules, so you pull in only the autoconfiguration you actually use.
- First-class Java 25 support, while keeping Java 17 as the floor.
- JSpecify null-safety baked into the framework APIs — compile-time nullability.
- API versioning and built-in resilience (
@Retryable,@ConcurrencyLimit) promoted into the framework itself. - Native OpenTelemetry trace propagation, and declarative HTTP interface clients replacing
RestTemplate/Feign. - Tighter GraalVM 24 / AOT alignment (which pairs with faster startup).
A “reset” means the assumptions change: the dependency graph, the null model, the default HTTP client, the resilience story. You can’t treat this like 3.2 → 3.3. Scope it as a migration project with a phased plan — but know the payoff is deleting glue, not just chasing compile errors.
# The Architecture Perspective
The most consequential architectural shift is that resilience, versioning, and observability moved from “libraries you assemble” to “framework you configure.” For years, a production Spring service was Spring Boot plus a constellation of third-party choices: Resilience4j for retries and bulkheads, Feign or a hand-rolled RestTemplate wrapper for HTTP clients, a manual Micrometer/OTel bridge for tracing. Each was a decision, a dependency, and a maintenance surface.
Spring Boot 4 pulls the common cases into the core. That changes your architecture diagram: fewer boxes, fewer version-compatibility matrices, one opinionated way to do the things every service needs.
flowchart LR
subgraph Before["Spring Boot 3.x + assembled glue"]
A["Feign"]
B["Resilience4j"]
C["manual OTel bridge"]
D["fat starters"]
end
subgraph After["Spring Boot 4.x built-ins"]
E["HTTP interface client"]
F["@Retryable / @ConcurrencyLimit"]
G["native OTel propagation"]
H["70+ focused modules"]
end
Before ==migrate==> After
classDef old fill:#d97706,color:#fff,stroke:#92400e;
classDef new fill:#059669,color:#fff,stroke:#065f46;
class A,B,C,D old;
class E,F,G,H new;The 70-module split is the other architectural story, and it cuts both ways. On the upside, your deployable pulls in less: smaller jars, a tighter dependency graph, a smaller attack surface, and better AOT/native results because there’s less to analyze. On the downside, a “fat starter” you leaned on is now several jars, and until your dependency set settles you’ll meet a round of “no autoconfiguration found.” This is dependency archaeology, not a code rewrite — plan for it as such.
Sequence the rollout by dependency depth: upgrade leaf services (few internal consumers) first, prove the pattern, then move shared libraries last. A shared lib upgraded too early forces every consumer to move on your timeline instead of theirs.
# The Developer Perspective
Let’s walk the changes you’ll actually type.
Null-safety with JSpecify. Framework 7 annotates its APIs with JSpecify, so your IDE and build can flag nullability mismatches at compile time. Turn it on per module and treat the first wave of warnings as findings, not noise — they’re pointing at real NPE-prone seams:
import org.jspecify.annotations.Nullable;
// The framework now tells the compiler what may be null.
public @Nullable User findByEmail(String email) {
return repository.findByEmail(email); // may return null — callers are warned
}API versioning is first-class. No more hand-rolled header parsing or path hacks:
@GetMapping(value = "/users/{id}", version = "2") // Framework 7 API versioning
public UserV2 getUserV2(@PathVariable Long id) { ... }Resilience is an annotation, not a dependency. Replace a Resilience4j retry and bulkhead with framework built-ins:
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 200))
@ConcurrencyLimit(10) // caps concurrent invocations — bulkhead, built in
public PricingQuote fetchQuote(String sku) {
return pricingClient.quote(sku);
}Declarative HTTP clients replace RestTemplate/Feign — and trace for free. Define an interface; Spring generates the client and propagates trace context to your OTel collector without manual plumbing:
@HttpExchange("/pricing")
interface PricingClient {
@GetExchange("/quote/{sku}")
PricingQuote quote(@PathVariable String sku); // trace IDs propagate automatically
}Run the Spring Boot migrator / OpenRewrite recipes before hand-editing anything. The bulk of the javax → jakarta moves and property-key renames are mechanical — let the tool churn through them so you can spend your attention on the JSpecify findings and the client refactors that actually need judgment.
Two predictable snags: Jackson 3 changes some serialization defaults (audit your custom serializers and date handling), and the 70-module split means a starter you relied on is now several jars — expect “class not found” until the dependency set settles. Neither is hard; both are surprising if you don’t expect them.
# The DevOps Perspective
The migration touches your pipeline and your runtime, not just your source.
Test suites get faster — measure it. Framework 7 introduces test context pausing: Spring can pause and resume contexts held in the context cache, saving memory and speeding up large test runs. On a big suite that’s real CI wall-clock and memory back. Capture the before/after so the migration’s cost shows a return.
Startup and memory improve via AOT. Spring Boot 4 aligns with GraalVM 24 and has stronger AOT processing — faster builds, lower startup memory, better native results. If cold start or autoscale-out latency matters to you, this migration is also a performance upgrade (and pairs with the JDK’s own startup work).
flowchart TD
P["Dependency inventory<br/>+ OpenRewrite recipes"] --> Q["Leaf services first"]
Q --> R["Shared libraries last"]
R --> S["Enable JSpecify per module"]
S --> T["Replace glue with built-ins"]
T --> U["Measure: config removed,<br/>startup, CI test time"]
classDef step fill:#2563eb,color:#fff,stroke:#1e40af;
classDef gate fill:#d97706,color:#fff,stroke:#92400e;
classDef win fill:#059669,color:#fff,stroke:#065f46;
class P,Q,R step;
class S,T gate;
class U win;Fewer moving parts to patch. Every third-party library you delete (Resilience4j, Feign, a manual OTel bridge) is one fewer CVE feed to watch and one fewer version to reconcile at the next Spring bump. That’s an ongoing operational dividend, not a one-time win.
Have a rollback posture. Because this is a baseline reset, keep the previous version deployable behind a flag or a parallel track until the new one has soaked in staging under real traffic — especially for the services carrying the trickiest Jakarta or Jackson changes.
Pros
- Deletes glue: resilience, HTTP clients, OTel wiring move into the core
- Smaller jars and tighter dependency graph from the 70-module split
- JSpecify surfaces real nullability bugs at compile time
- Faster CI via test-context pausing
- Better startup/memory through GraalVM 24 + AOT
Cons
- Jakarta EE 11 + Jackson 3 changes require an audit
- Fat starters fragment into several jars — expect transient 'class not found'
- JSpecify's first warning wave is real work to triage
- A shared-lib upgrade forces consumers to move
- It's a project, not an afternoon
# Putting It Together
Spring Boot 4 is a “worth it, but read the map first” upgrade. The scary part — 70 modules, Jakarta EE 11, a new null model — is mostly mechanical or one-time. The lasting part is that the framework now owns the resilience, versioning, and observability code you used to assemble and maintain yourself. You migrate to less code. Sequence it leaves-first, let the tooling do the mechanical churn, and measure the glue you delete and the CI time you save so the effort shows a return.
# References
- Spring Boot 4.0.0 available now — Spring.io
- Spring Boot 4.1.0 available now — Spring.io
- Spring Boot 4 & Spring Framework 7 – What’s New — Baeldung
- The Spring Team on Spring Framework 7 and Spring Boot 4 — InfoQ
- A Comprehensive Analysis of Spring Boot 4 and Spring Framework 7 — CodeTutorials
- Java 26 + Spring Boot 4: From Certification Skills to Production-Ready Apps — JAVAPRO
Click to show appreciation
Discussion