Spring Framework 7 & Spring Boot 4: Unveiling a New Era for Java Development
Spring Framework 7 and Spring Boot 4 mark a new era for Java. Explore key updates: Jakarta EE 11, JSpecify null-safety, integrated resilience, build-time optimizations, and evolving Spring AI.
JVM Weekly vol. 153: Spring Framework 7 and Spring Boot 4: The Tastiest Bites
Authored by Artur Skowroński, Published Nov 20, 2025
While I typically reserve detailed new release discussions for "Rest of the Story," the monumental Spring launch demands special attention. This week, Spring (adhering to its official calendar) unveiled a new generation across its flagship projects: Spring Framework 7, Spring Boot 4, Spring Data 2025.1, and Spring AI 1.1 (designed for the Boot 3.5 line, with 2.x actively under development for Spring Boot 4).
Major version increments like these are never accidental; they often signify the end of one era and the beginning of another, rather than just another patch. This new Spring iteration aligns with a much broader transformation sweeping across the entire Java ecosystem. Java has seen new LTS releases, Jakarta EE has successfully completed its javax to jakarta migration, GraalVM has matured beyond a "conference talk curiosity," and Artificial Intelligence has rapidly moved from hackathons directly into organizational budgets, roadmaps, and key performance indicators (KPIs). (Though, admittedly, the KPI aspect can be the most challenging part).
Crucially, Spring isn't merely attempting to keep pace; it's clearly aiming to co-define this ongoing technological movement.
Instead of introducing yet another "compatibility flag" or adding a new section to application.yml, the team opted for a coordinated, one-time "Big Bang" release. This initiative ensures that the platform's foundations are aligned with new standards, a significant portion of technical debt has been addressed, and Spring is strategically positioned for the AI-driven years ahead. From an external perspective, it appears to be a version migration. Internally, however, it represents a fundamental redesign of Spring's entire mental model.
The Foundation: Goodbye javax
At a fundamental level, the changes are quite concrete. The javax world officially disappears from the main development path. Annotations such as @Resource, @PostConstruct, or @Inject – which have been present in almost every project for years – now require migration to the new jakarta namespace.
Before:
import javax.annotation.PostConstruct;
import javax.persistence.Entity;
@Entity
public class LegacyUser {
@PostConstruct
private void init() { ... }
}
These are now seamlessly integrated into a consistent, Jakarta-native world that is compliant with Jakarta EE 11.
After:
import jakarta.annotation.PostConstruct;
import jakarta.persistence.Entity;
@Entity
public class ModernUser {
@PostConstruct
private void init() { ... }
}
Another significant "hidden landmine" in the plumbing is Jackson 3. Much like the Jakarta migration, the new Jackson version changes package names. Spring Framework 7 performs considerable heavy lifting to support a mix of Jackson 2 and 3 for now, but the signal is clear: the old JSON backend is on its way out, indicating another ecosystem-wide shift.
Concurrently, the entire stack receives an upgrade: newer Servlet, JPA, and Bean Validation versions, plus new generations of Tomcat and Jetty. Some well-known servers, like Undertow, simply didn't make the cut for the new standard and have consequently dropped out of the Spring ecosystem. While this can be painful for long-time users (I recall regularly using Undertow during my Clojure days), it marks the closure of a chapter. This leap was inevitable; the only question was whether it would be a single, planned surgical cut or a "death by a thousand cuts" through endless, frustrating compatibility fractures. Spring opted for the former.
The Billion Dollar Mistake and Other Improvements for Sanity's Sake
The second axis of change focuses on improving the daily developer experience. This begins with a problem we all know too well: nulls. Spring Framework 7 and the new generation of libraries surrounding it (Spring Data, Spring Security, Spring Vault) are now adopting JSpecify.
To grasp the magnitude of this change, consider the absolute chaos we've navigated for the past 15 years. We contended with javax.annotation.Nullable, org.jetbrains.annotations.Nullable, edu.umd.cs.findbugs.annotations.Nullable, and android.support.annotation.Nullable. It was the Wild West, where various tools (IDEs, Sonar) frequently ignored each other's annotations—a truly personal level of hell.
JSpecify represents a peace treaty. It is a standard agreed upon by giants like Google, JetBrains, Oracle, and others, finally enabling a common language for nullability.
In Spring Framework 7, this manifests as a massive "Inversion of Control" for nulls. Thanks to the @NullMarked annotation applied at the package level, Spring ceases to be "everything can be null" by default. Instead, it sends a clear signal: non-null is the normal case. You no longer have to clutter your code with @NonNull on every parameter; you only explicitly mark the exceptions with @Nullable.
@org.jspecify.annotations.NullMarked
package com.example.billing;
public class BillingService {
// The compiler knows 'invoice' cannot be null.
public Receipt processPayment(Invoice invoice) {
return new Receipt(invoice.getId());
}
public @Nullable Transaction findHistory(@Nullable String transactionId) {
return transactionId != null ? repo.find(transactionId) : null;
}
}
We can anticipate an end to the "hacky workarounds" we've been employing for over a decade.
First, API Versioning is finally a first-class citizen. Developers will no longer need to write custom interceptors or incorporate external libraries just to version their REST endpoints. Whether you prefer path-based, header-based, or query-param versioning, it is now natively supported through standard annotations.
@RestController
@RequestMapping("/orders")
public class OrderController {
// Native API Versioning support (Header, Path, or Param)
// No more custom interceptors needed
@GetMapping
@ApiVersion(value = "1.0", strategy = VersionStrategy.HEADER)
public List<Order> getOrdersLegacy() {
return repository.findAll();
}
// Resilience patterns moved directly into Core
// No extra 'spring-retry' dependency required
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500))
@GetMapping
@ApiVersion(value = "2.0", strategy = VersionStrategy.HEADER)
public List<Order> getOrdersResilient() {
return service.fetchOrdersWithRateLimit();
}
}
Second, Resilience patterns (Retries, Circuit Breakers, Rate Limiters) are being moved and promoted directly into spring-core and the main framework. Features like @Retryable and Rate Limiting are now part of the core. This means you no longer need to pull in extra dependencies or rely solely on external tools like Resilience4j for basic robustness. In distributed systems, failures are a default state (a knowledge I hope everyone has internalized by now), and the framework finally treats them as such "out of the box."
Less Magic, More Build-Time Actions
The third axis of change pertains to Spring's relationship with the compiler and the build process. For years, Spring was the master of "runtime magic": classpath scanning, dynamic configuration, reflection, and proxies. This provided incredible flexibility but came with consequences. While it often felt like technology indistinguishable from magic (a nod to Arthur C. Clarke), in the era of containers, cold starts, and native images, this approach began to hurt.
Following current trends in the ecosystem, the new Spring consistently shifts the workload to build time. Instead of performing "everything" at startup, it strives to know and generate as much as possible beforehand.
This shift is best observed in Spring Boot 4. The existing spring-boot-autoconfigure, which had become bloated over the years, has been refactored into a neat set of smaller modules. Suddenly, your IDE stops inundating you with suggestions for classes and configuration properties that aren't even on your classpath. This is precisely where the Spring AOT (Ahead-of-Time) world converges with what's happening inside the Virtual Machine itself within Project Leyden.
Project Leyden, part of OpenJDK, shares the exact same goal, albeit at a lower level: to improve startup time, time-to-peak performance, and application footprint by moving work from execution time to an earlier stage. JDK 24 brought the first installment of this philosophy with JEP 483: Ahead-of-Time Class Loading & Linking, which can load and link classes during a "training" run and save the result in a special cache.
When you combine this with modular Boot 4 and Spring AOT, the puzzle pieces begin to fit. By slicing up autoconfiguration and moving "magic" parts to build-time generated code, Spring practically reduces the scope that the JVM needs to warm up at startup. Leyden, with its AOT cache, no longer operates on a chaotic clump of classes, but on a predictable, slimmed-down graph that Spring has already cleaned and materialized.
It's no wonder early experiments show significant synergy: even before Leyden, Spring AOT optimizations alone yielded about a 15% startup boost on the classic JVM, and "pre-main" improvements from Leyden can boost this effect even further. In practice, this means that Java – the same Java we once characterized as "heavy" – is beginning to behave more like an ecosystem with built-in, hybrid AOT: part of the work is done by the framework, and part by the JVM. The container rises faster, consumes less RAM, and relies less on dynamic generation occurring at startup. Native builds have less matter to analyze, and AOT is presented with small, well-described blocks to work with instead of one mega-JAR.
Interestingly, we can also add Spring Data 2025.1 to this, with repositories prepared Ahead-of-Time. Methods like findByEmailAndStatus cease to be mysterious spells interpreted at application startup and become regular, generated code, compiled along with the rest of the system. Startup is faster, fewer things happen "in the background," and behavior in native images ceases to be a lottery.
A Tale of Two AI Streams
However, a no less interesting part of this puzzle concerns AI. Spring AI is clearly diverging into two distinct development lines.
The 1.1 branch concludes the Spring Boot 3.5 era: it's a "broadening" release, not a fundamental table-flipper. In practice, this means you can already build sensible agentic workflows within the 3.5 ecosystem: you incorporate the Spring AI starter, configure a provider, define a few tools, and the rest – from the MCP (Model-Controller-Presenter) skeleton, through JSON mapping, to integration with ChatClient – is delivered by the framework. Spring AI 1.1 is "AI for today's projects" – maximally compatible with existing Boot, focused on stability, use-case coverage, and integration with your current monoliths and microservices.
Simultaneously, the team is actively working on the 2.x line, which deliberately breaks this compromise and is designed in tandem with Spring Boot 4. Here, the theme is redesign, not just expansion: full compatibility with Boot 4, a new API shape, separation of reactive and imperative ChatClient, preparation for JSpecify and null-safe contracts (which Spring officially announces as a goal for 2.0), and better embedding of MCP and AOT deep within the architecture. For the developer, the difference boils down to direction: 1.1 is the fast track to add AI to an existing 3.5 architecture, but 2.x represents a conscious decision that you are building a system where LLMs (Large Language Models), MCP agents, and classic Spring services create one coherent, first-class stack.
@Service
public class BookingAgent {
private final ChatClient chatClient;
public BookingAgent(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("You are a booking assistant.")
.defaultTools("bookingTools") // References a bean, not just a function
.build();
}
// Defined once, used by Agents and LLMs "automagically"
@Tool(description = "Check room availability for a given date range")
public boolean checkAvailability(@NonNull LocalDate from, @NonNull LocalDate to) {
return bookingService.hasSlots(from, to);
}
}
I suspect the difference in capabilities will only widen – if only to motivate people to migrate to new versions 😉.
The moment you realize your technology of choice can't support the needs of projects.
To sum it up – it's very clear that Spring has delineated two parallel realities.
Spring 6 + Boot 3 is now the stable set – stable, known, and ideal for systems entering the maintenance phase that simply need to work reliably, not chase every novelty.
On the other side, we have the new baseline: Spring Framework 7, Spring Boot 4, fresh Spring Data, and Spring AI 2.x. This is the domain where Jakarta EE 11, JSpecify, AOT, vector search, MCP agents, and lightweight runtimes originating from Spring Boot 4.0 await.
Honestly? The scope of these changes is large enough that I anticipate many serious projects on Spring 6 will simply remain there – at least for a few years. It will be a very comfortable haven: support is available, everything is familiar, and business requirements are met. But that's precisely why the question is no longer "is it worth upgrading?" The real question is: in which world do you want your system to live in five years – in the safe, tamed 6/3, or on the new, wilder, but also much more promising 7/4 terrain?
I have my suspicions about where the most ambitious teams will eventually land – or those who prefer to migrate gradually, avoiding a major "Big Bang" when support inevitably ends... but we'll see.
They are really trying!
PS: There was no edition last week as I was recently traveling, which provided an opportunity to catch up on some bucket list TV Shows on the plane. And as a pre-Disney Star Wars fan... OMFG.
PS2: Next week I’ll be speaking at #KotlinDevDay in Amsterdam! See you there 😊