Spring Bootspring-bootspring-boot-4spring-framework-7javajspecifyapi-versioningbackend-engineering

Spring Boot 4 and Spring Framework 7: What Actually Changed

Spring Boot 4.0 and Spring Framework 7.0 landed together in November 2025 — the first major Spring release in three years. Null-safety via JSpecify, built-in API versioning, a modularized codebase, Jackson 3, and HTTP interface clients. Here is what matters for a production Spring shop, and what upgrading actually costs.

August 23, 2026
10 min read

Spring Boot 3.0 shipped in November 2022 and cost everyone a weekend: the javaxjakarta namespace migration touched every import in every service I owned. So when Spring Boot 4.0 and Spring Framework 7.0 landed together in November 2025, my first question wasn't "what's new" — it was "how much will this hurt."

The good news: nothing in this release is a javaxjakarta. There's no single mechanical change that rewrites your whole codebase. The bad news is subtler — Boot 4 raises the floor on Java and Jakarta EE, bumps a pile of third-party majors at once (Jackson being the loud one), and reorganizes its own module layout. It's a dependency migration rather than a source migration, which is easier but harder to predict.

Here's what's actually in it, ordered by how much it'll change your day-to-day.

The Baselines First

Before any feature, the constraints, because they decide whether you can upgrade at all:

Spring Boot 3.xSpring Boot 4.0
Minimum Java1717 (25 recommended)
Jakarta EE9/1011
Kotlin1.x supported2.x
Jackson2.x3.x
Spring Security6.x7.x

The Jakarta EE 11 baseline is the one that bites indirectly: it pulls Servlet 6.1, Hibernate 7, and friends. If you're sitting on a servlet container or app server that hasn't shipped Jakarta EE 11 support, that's your blocker, not Spring.

⚠️

Spring Boot 3.5.x remains the version to stay on if you can't clear those baselines. Plan the upgrade against your OSS support window rather than against the release announcement — running an unsupported Boot line is a security problem, not a tidiness problem.

Null-Safety That the Compiler Can See (JSpecify)

This is the change I've gotten the most mileage out of, and it's the one that's easiest to ignore.

Spring has annotated nullability for years with its own @Nullable / @NonNull annotations. The problem was that they were Spring's, so only tools that specifically knew about Spring could act on them. Framework 7 replaces that with JSpecify — a vendor-neutral annotation set backed by Google, JetBrains, Oracle, Microsoft, and Spring — applied across the entire Spring codebase.

The practical shape: packages are declared @NullMarked, meaning everything is non-null unless explicitly marked @Nullable. So this:

java
import org.jspecify.annotations.Nullable;
 
@NullMarked
public class OrderService {
 
    public Order findOrder(String id) { ... }          // never returns null
 
    public @Nullable Order findOptionalOrder(String id) { ... }  // may return null
}

...gives IntelliJ, NullAway, and the Kotlin compiler enough information to flag a bad dereference before it ships. In Kotlin, Spring APIs now map to accurate platform types instead of everything being Type!, which removes a whole category of "it compiled, then NPE'd at runtime."

My honest take after using this on two services: the value is real but it's a ratchet, not a switch. Turn on a null-checker in CI for new code, fix what it finds in modules you're touching anyway, and don't try to annotate a large legacy codebase in one PR.

Built-In API Versioning

Every team I've worked with has hand-rolled API versioning: a URL prefix here, a custom HandlerMapping there, a header-sniffing filter someone wrote in 2019 and nobody wants to touch. Framework 7 puts it in the framework for both Spring MVC and WebFlux.

java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
 
    @GetMapping(version = "1.0")
    public OrderV1 getOrderV1(@PathVariable String id) { ... }
 
    @GetMapping(version = "2.0")
    public OrderV2 getOrderV2(@PathVariable String id) { ... }
}

Where the version comes from is configuration, not code — a request header, a query parameter, a media-type parameter, or a path segment. That separation is the actual win: you can move from /api/v1/... to an X-API-Version header without rewriting controllers, and the RestClient / WebClient side can be configured to send a default version so callers don't repeat themselves.

It also handles version resolution sensibly — a request for 1.3 can route to the 1.0 handler when no exact match exists, rather than 404ing. If you've built this yourself, you know that's the part you got wrong the first time.

HTTP Interface Clients, Promoted

Declarative HTTP clients — define an interface, let Spring generate the implementation — existed in Framework 6 but needed manual proxy-factory wiring. Boot 4 makes them a first-class, auto-configured citizen:

java
@HttpExchange(url = "/inventory")
public interface InventoryClient {
 
    @GetExchange("/{sku}")
    StockLevel getStock(@PathVariable String sku);
 
    @PostExchange("/reserve")
    Reservation reserve(@RequestBody ReservationRequest request);
}

Annotate your configuration with @ImportHttpServices, point a base URL at it in application.yml, and inject InventoryClient anywhere. This is Spring absorbing what OpenFeign has been doing for a decade, minus the extra dependency and the Netflix-era baggage. For new service-to-service calls I'd now default to this over a hand-written RestClient wrapper.

Resilience Without a Second Library

Retries and concurrency limits move into Spring Framework core:

java
@Retryable(maxAttempts = 3, delay = 200, multiplier = 2.0)
public PaymentResult charge(Order order) { ... }
 
@ConcurrencyLimit(10)
public Report generateReport(ReportRequest request) { ... }

Be clear-eyed about the scope here. This is not a Resilience4j replacement — there's no circuit breaker, no bulkhead metrics, no rich fallback model. It's the 80% case that most services actually need, available without adding a dependency and its configuration surface. If you're currently pulling in Resilience4j purely for @Retry, you can probably drop it. If you're using circuit breakers properly, keep it.

The Modularization

Boot 4 splits spring-boot-autoconfigure — historically one enormous jar that knew how to configure everything — into many focused modules, each owning its own auto-configuration. So Jackson support lives in a Jackson module, JPA support in a JPA module, and so on.

For most applications using starters, this is invisible. Starters pull the right modules and you never notice. It matters if you:

  • Depend directly on spring-boot-autoconfigure rather than a starter
  • Write your own auto-configuration and reference Boot's internals
  • Exclude specific auto-configuration classes by name (some class names moved)

The payoff is a smaller dependency graph per app and — the real motivation — a codebase Spring can evolve module-by-module instead of shipping one monolithic jar forever.

Jackson 3: The Migration You'll Actually Feel

Boot 4 moves to Jackson 3, and this is where the upgrade stops being free. Jackson 3 changed its base package (com.fasterxml.jacksontools.jackson for most modules), made some defaults stricter, and reworked parts of the builder API.

If your code only touches Jackson through Spring's abstractions — @RestController, HttpMessageConverter, @JsonProperty on DTOs — you'll likely sail through; the jackson-annotations package notably stayed put. If you have custom serializers, a hand-configured ObjectMapper, or a library that embeds Jackson 2 types in its public API, budget real time.

Order of operations that worked for me: get to Boot 3.5 and Java 25 first, run with -Xlint:deprecation and clean up what Boot 3.5 deprecates, then jump to 4.0. Boot 3.5 was explicitly built as the on-ramp — most 4.0 breaking changes were deprecated there first, so the deprecation warnings are your migration checklist.

What Got Removed

Spring Boot 4 uses a major version to take out the trash. The notable removals and deprecations:

  • Deprecated 3.x APIs — gone, as promised. That's what the 3.5 deprecation pass was for.
  • spring-boot-devtools is deprecated, with the ecosystem pointing at JVM hot-reload tooling instead.
  • Older observability shims — Boot 4 leans entirely on Micrometer and the OpenTelemetry bridge.
  • Legacy RestTemplate-centric patternsRestTemplate still exists, but everything new is built around RestClient. Treat RestTemplate as maintenance-only in new code.

Should You Upgrade Now?

My read, depending on where you sit:

  • Greenfield service, Java 25 available? Start on Boot 4. No migration cost, and you get null-safety and API versioning for free.
  • Established service on Boot 3.5, healthy dependency tree? Plan it for a normal quarter. The work is real but bounded, and it's mostly dependency-compatibility triage, not rewriting.
  • On Boot 3.0–3.4, or dragging old third-party libraries? Get to 3.5 first. Doing 3.0 → 4.0 in one jump means debugging two migrations at once with no clean bisect.
  • Stuck below Jakarta EE 11 because of your runtime? Fix that first. It's the hard blocker and nothing else you do matters until it clears.

The thing worth internalizing is what this release is. Boot 3.0 was a forced migration — you moved because javax stopped existing. Boot 4.0 is a foundation release: modular internals, neutral null-safety annotations, versioning and resilience primitives that used to be everyone's homework. Nothing here forces your hand this quarter. All of it makes the next three years of Spring development better, which is exactly the kind of release that's easy to postpone for too long.

Frequently asked questions

Does Spring Boot 4 require Java 25?

No. The minimum is Java 17, the same as Spring Boot 3. Java 25 (the current LTS) is recommended because features like virtual threads, scoped values, and compact object headers give the best runtime behavior, but Java 17 remains supported.

Is the Spring Boot 4 upgrade as painful as Spring Boot 3?

Generally no. Spring Boot 3 forced the javax-to-jakarta namespace migration, which mechanically touched every file. Spring Boot 4 has no equivalent source-level change — the work is dependency compatibility, with Jackson 3's package rename being the most likely source of real effort.

What is JSpecify and why did Spring adopt it?

JSpecify is a vendor-neutral set of nullability annotations backed by Google, JetBrains, Oracle, Microsoft, and Spring. Spring replaced its own proprietary @Nullable/@NonNull annotations with it so that any compatible tool — IntelliJ, NullAway, the Kotlin compiler — can enforce null-safety against Spring APIs.

Do I still need Resilience4j with Spring Framework 7?

It depends on what you use it for. Framework 7 adds @Retryable and @ConcurrencyLimit in core, which covers basic retries and concurrency capping. It does not include circuit breakers, bulkheads, or rate limiters, so keep Resilience4j if you rely on those.

How long will Spring Boot 3.x be supported?

Spring Boot 3.5 is the final 3.x feature line and continues on the standard OSS support window, with commercial support available beyond that. Check the official Spring support policy page for exact end dates before planning your migration, since they shift with the release calendar.


Related reading: the Spring Boot roadmap and the Backend Engineer roadmap covers where Spring sits in a full backend curriculum, and the Spring AI guides go deep on building LLM features on top of this stack.

More from Spring Boot

Browse more articles and guides on this topic.