
Spring Boot 4 and Spring Framework 7 represent a major leap forward for Java developers. This is the guide I keep coming back to and updating: what's in Spring Boot 4, what landed in 4.1, and what you need to know to move.
If you're on Spring Boot 3, this stopped being optional in June 2026. Every 3.x branch is now out of OSS support, so 4.x is where free security patches live. I wrote about that in detail in Spring Boot End of Life.
Which Spring Boot 4 Version Should You Be On?
Two versions of Spring Boot 4 are out, and only one of them is a good place to land.
| Branch | Released | End of OSS Support |
|---|---|---|
| 4.1.x | 2026-06 | 2027-07 |
| 4.0.x | 2025-11 | 2026-12 |
Go to 4.1. 4.0 loses OSS support in December 2026, so starting there means doing the upgrade twice. Every branch gets roughly 13 months of free patches, which is the cadence worth planning around rather than being surprised by.
Two things that make this upgrade easier than you'd expect:
The Java baseline hasn't moved. Spring Boot 4 requires Java 17, the same as Spring Boot 3, with first-class support for Java 25 if you want it. There's no forced JDK jump hiding in here.
There's no javax to jakarta rewrite. That was the thing that made 2.7 to 3.0 hurt, and it isn't in this one. Spring Boot 4 moves to a Jakarta EE 11 and Servlet 6.1 baseline, so there's real work in the dependency layer, but nothing like the last major upgrade.
High-Level Overview
Spring Framework 7, Spring Boot 4, and the broader Spring ecosystem focus on three core themes: developer experience, performance, and production-readiness.
The developer experience improvements are immediately noticeable. Features like HTTP Interface Clients and the REST Test Client eliminate boilerplate code that we've written countless times. The new Programmatic Bean Registration API brings a modern, fluent approach to dynamic bean creation. And first-class API versioning means you no longer need workarounds for a common enterprise requirement.
Performance gets a significant boost through Spring Data AOT, which moves query generation from runtime to compile-time, resulting in 50-70% faster startup times. Combined with continued improvements for GraalVM native images, Spring applications are more cloud-native than ever.
Production-readiness is enhanced with built-in resilience patterns (no external libraries needed) and official OpenTelemetry integration for observability. You also get compile-time null safety with JSpecify and Jackson 3's improved defaults.
There are many more improvements in these releases, but let's dive into 10 of my favorites in no particular order.
Resources:
New Features
1. Null Safety with JSpecify
Spring Framework 7 introduces first-class support for compile-time null safety using JSpecify annotations. Instead of runtime NullPointerException surprises, you catch issues during compilation.
The key is the @NullMarked annotation at the package level, which declares that all types in that package are non-null by default. For exceptions, you use @Nullable explicitly.
// package-info.java
@NullMarked
package dev.danvega.coffeeshop;
import org.jspecify.annotations.NullMarked;
// All parameters and return types are non-null by default
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// Explicitly mark nullable parameters
public List<User> search(@Nullable String name) {
if (name == null) {
return userRepository.findAll();
}
return userRepository.findByName(name);
}
Your IDE will warn you about potential null issues, and this also improves Kotlin interoperability.
Resources:
2. HTTP Interface Clients
Gone are the days of manually configuring HttpServiceProxyFactory. Spring Boot 4 introduces @ImportHttpServices for zero-configuration HTTP clients.
Before (Manual Configuration):
@Bean
public TodoService todoService(RestClient.Builder builder) {
var restClient = builder
.baseUrl("https://jsonplaceholder.typicode.com")
.build();
var adapter = RestClientAdapter.create(restClient);
var factory = HttpServiceProxyFactory.builderFor(adapter).build();
return factory.createClient(TodoService.class);
}
After (Spring Boot 4):
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(TodoService.class)
public class HttpClientConfig {
// That's it!
}
@HttpExchange(url = "https://jsonplaceholder.typicode.com", accept = "application/json")
public interface TodoService {
@GetExchange("/todos")
List<Todo> getAllTodos();
@GetExchange("/todos/{id}")
Todo getTodoById(@PathVariable Long id);
@PostExchange("/todos")
Todo createTodo(@RequestBody Todo todo);
}
The interface is now automatically turned into a bean with full type safety and compile-time checking.
Resources:
3. Programmatic Bean Registration
The new BeanRegistrar interface provides a modern, AOT-compatible way to register beans programmatically with runtime flexibility.
public class MessageServiceRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
String messageType = env.getProperty("app.message-type", "email");
switch (messageType.toLowerCase()) {
case "email" -> registry.registerBean("messageService",
EmailMessageService.class,
spec -> spec.description("Email service"));
case "sms" -> registry.registerBean("messageService",
SmsMessageService.class,
spec -> spec.description("SMS service"));
}
}
}
@Configuration
@Import(MessageServiceRegistrar.class)
public class AppConfig {
}
This replaces the verbose BeanDefinitionRegistryPostProcessor with a clean, lambda-based API that's also GraalVM native image compatible.
Resources:
4. API Versioning
Spring Framework 7 finally delivers first-class API versioning support using media type parameters.
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(version = "1.0")
public UserDTOv1 getUserV1(@PathVariable Long id) {
return new UserDTOv1(id, user.getName());
}
@GetMapping(version = "2.0")
public UserDTOv2 getUserV2(@PathVariable Long id) {
return new UserDTOv2(id, user.getFirstName(), user.getLastName());
}
}
@Configuration
public class ApiVersioningConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useMediaTypeParameterVersioning();
}
}
Clients request specific versions via the Accept header:
curl -H "Accept: application/json;version=1.0" http://localhost:8080/api/users/1
curl -H "Accept: application/json;version=2.0" http://localhost:8080/api/users/1
This also supports RFC-compliant deprecation headers for sunset planning.
Resources:
5. JMS Client API
A new fluent JMS API makes messaging with Apache ActiveMQ Artemis much cleaner than the traditional JmsTemplate.
@Service
public class OrderMessagingService {
private final JmsClient jmsClient;
// Basic send
public void sendOrder(Order order) {
jmsClient.send("orders.queue")
.withBody(order);
}
// With Quality of Service
public void sendPriorityOrder(Order order) {
jmsClient.send("orders.queue")
.withPriority(9)
.withTimeToLive(Duration.ofMinutes(5))
.withBody(order);
}
// Request-Reply pattern
public OrderConfirmation processOrder(Order order) {
return jmsClient.requestAndReceive("orders.queue")
.withBody(order)
.convertTo(OrderConfirmation.class);
}
}
The API supports all 7 essential messaging patterns including custom headers, synchronous receive, and reusable operation handles for performance.
Resources:
6. Built-in Resilience
Spring Framework 7 includes enterprise-grade resilience patterns without external libraries like Resilience4j.
@Configuration
@EnableResilientMethods
public class ResilienceConfig {
}
@Service
public class ExternalApiService {
// Exponential backoff with jitter
@Retryable(
maxAttempts = 4,
delay = 500,
multiplier = 2.0, // 500ms, 1s, 2s, 4s
maxDelay = 5000,
jitter = 100 // Prevents thundering herd
)
public String fetchData(String id) {
return externalApi.getData(id);
}
// Concurrency control
@ConcurrencyLimit(2)
public String performHeavyOperation(String taskId) {
// Only 2 executions at a time
return heavyComputation(taskId);
}
}
You can combine both annotations for single-threaded execution with automatic retry.
Resources:
7. Jackson 3 Support
Spring Boot 4 integrates Jackson 3 with improved defaults and a new immutable configuration model.
Key changes from Jackson 2:
- Dates: ISO-8601 strings by default (not numeric timestamps)
- Exceptions: Unchecked for better lambda/stream integration
- Packages:
tools.jacksoninstead ofcom.fasterxml
// JSON Views for flexible responses
public class Views {
public interface Summary {}
public interface Public extends Summary {}
public interface Internal extends Public {}
}
public record Donut(
@JsonView(Views.Summary.class) Long id,
@JsonView(Views.Summary.class) String name,
@JsonView(Views.Public.class) BigDecimal price,
@JsonView(Views.Internal.class) Integer stockCount
) {}
@RestController
@RequestMapping("/api/donuts")
public class DonutController {
@GetMapping
@JsonView(Views.Summary.class)
public List<Donut> getSummary() {
return donutService.findAll();
}
@GetMapping("/internal")
@JsonView(Views.Internal.class)
public List<Donut> getInternal() {
return donutService.findAll();
}
}
Resources:
8. REST Test Client
Say goodbye to juggling MockMvc, WebTestClient, and TestRestTemplate. The new RestTestClient provides one consistent API for all test types.
// Unit Test - Fast, isolated
@ExtendWith(MockitoExtension.class)
class TodoControllerTest {
@Mock
private TodoService todoService;
private RestTestClient client;
@BeforeEach
void setUp() {
client = RestTestClient.bindToController(
new TodoController(todoService)
).build();
}
@Test
void shouldGetAllTodos() {
when(todoService.findAll()).thenReturn(
List.of(new Todo(1L, "Learn Spring", false))
);
client.get()
.uri("/api/todos")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].title").isEqualTo("Learn Spring");
}
}
// E2E Test - Full stack with real HTTP
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class TodoIntegrationTest {
@LocalServerPort
private int port;
@Test
void shouldCreateTodo() {
RestTestClient client = RestTestClient.bindToServer()
.baseUrl("http://localhost:" + port)
.build();
client.post()
.uri("/api/todos")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new Todo(null, "Integration test", false))
.exchange()
.expectStatus().isCreated();
}
}
Choose from 5 binding approaches: bindToController, bindToMockMvc, bindToApplicationContext, bindToServer, and bindToRouterFunction.
Resources:
9. Spring Data AOT
Spring Data AOT moves query generation from runtime to compile-time, resulting in 50-70% faster startup and reduced memory usage.
public interface CoffeeRepository extends CrudRepository<Coffee, Long> {
// AOT generates SQL at compile-time
List<Coffee> findByNameContainingIgnoreCase(String name);
List<Coffee> findByRoastLevelAndOrigin(String roastLevel, String origin);
@Query("SELECT * FROM coffee WHERE price < :maxPrice ORDER BY price")
List<Coffee> findAffordableCoffees(BigDecimal maxPrice);
}
Benefits include:
- Early error detection (catch query typos at build time)
- GraalVM native image ready
- Pre-compiled repository implementations
- Inspectable generated code in
target/spring-aot/
Build with AOT processing:
./mvnw clean package
ls target/spring-aot/main/sources/
Resources:
10. OpenTelemetry Integration
Spring Boot 4 provides official, production-ready OpenTelemetry support with a single dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
spring:
application:
name: my-service
management:
tracing:
sampling:
probability: 1.0
otlp:
tracing:
endpoint: http://localhost:4318/v1/traces
You get automatic instrumentation for HTTP requests, database calls, and log correlation:
2025-11-18 10:30:45 INFO [traceId=abc123, spanId=def456] Processing order...
Add custom spans with @Observed:
@Service
public class OrderService {
@Observed(name = "order.process", contextualName = "processOrder")
public Order processOrder(OrderRequest request) {
return orderRepository.save(createOrder(request));
}
}
Resources:
11. Modularization
This is the one that'll actually change your build file. Spring Boot 4 splits the framework into smaller, more focused modules, which means some auto-configuration you took for granted now lives somewhere you have to ask for explicitly.
If you upgrade and something that "just worked" suddenly doesn't, this is usually why. It's the single change most likely to bite you on day one, and it's worth understanding before you start rather than after.
Resources:
What's New in Spring Boot 4.1
Spring Boot 4.1 landed in June 2026. It's a smaller release than 4.0, but there are a few things worth moving for.
Spring gRPC brings first-class gRPC support to Spring Boot. If you've been wiring this up by hand, this is the release you've been waiting for.
Lazy JDBC Connections is one property and a real performance win. Connections are only acquired when actually needed rather than at the start of every transaction.
SSRF Protection with InetAddressFilter closes a whole class of attack with a single bean. If your app fetches user-supplied URLs, read this one. It's also a good example of why staying current matters: this is a security feature you simply do not get on 3.5, and never will.
The New @RedisListener Annotation does for Redis messaging what @KafkaListener did for Kafka.
Type-Safe Property Paths in Spring Data replaces stringly-typed property references with something the compiler can check.
Upgrading from Spring Boot 3
Start with the Spring Boot 4.0 Migration Guide. It's thorough and it's the official word.
Two things to know before you begin. Modularization (above) is the most likely source of surprise. And if you're running this across a lot of services rather than one, that's a different kind of problem than a single upgrade.
If you want to see what a 4.1 project looks like before committing to anything, start.spring.io will generate one in about fifteen seconds.
Conclusion
Spring Framework 7 and Spring Boot 4 deliver significant improvements across developer experience, performance, and production-readiness. The theme is clear: less boilerplate, faster applications, and better defaults.
My personal highlights:
- Null Safety with JSpecify catches errors at compile-time instead of runtime
- HTTP Interface Clients and API Versioning eliminate code I've written hundreds of times
- Spring Data AOT dramatically improves startup time
- Built-in Resilience removes external dependencies for common patterns
- REST Test Client finally unifies our testing story
Ready to get started? Head to start.spring.io, select Spring Boot 4.1, and start exploring. I've also created a discovery repository with links to all the individual projects and resources mentioned in this post.
And if you're still on 3.x, the clock already ran out. Every 3.x branch lost OSS support in June 2026.
If you want to dive deeper into any of these features, check out my YouTube channel where I have dedicated tutorials for some of the features listed in this article.
Happy coding, friends!
Related Articles
Spring Boot End of Life: Every 3.x Branch Is Now Unsupported
Spring Boot 3.5 reached end of life in June 2026 and every 3.x branch is now unsupported. What that means, and how to plan your Spring Boot 4 upgrade.
Spring Boot 4 Modularization: Fix Missing Auto-Configuration
Spring Boot 4 broke your auto-configuration? Learn which modular starters you now need—Flyway, H2, and the test starters behind @AutoConfigureMockMvc.
Native Retry Support in Spring Boot 4: No External Dependencies Required
Spring Boot 4 includes native retry support with @Retryable, RetryTemplate, and @ConcurrencyLimit. Learn how to add resilience without the spring-retry dependency.
$ subscribe --list weeklyish
Enjoyed this post? Get the next one.
Java, Spring, and what I'm learning — one email, weekly(ish). Unsubscribe anytime.