Spring Boot 4 and Spring Framework 7: The New Era of Java for Fast and Secure Microservices
Spring Boot 4.0 shipped in November 2025 alongside Spring Framework 7.0, and it changes more than version numbers. The framework went through a serious modularization effort, doubled down on AOT compilation, and cleaned up APIs that were showing their age. If you build Java services for a living, this release deserves your attention.
Here's what actually changed.
minimum requirements
Spring Boot 4 raises the floor. You need Java 17 at minimum, but honestly, run it on Java 21 or 25 if you can. The reason is Virtual Threads, which let you write straightforward blocking code and still get concurrency that rivals reactive WebFlux. The framework also aligns fully with Jakarta EE 11, which means Servlet 6.1, JPA 3.2, and Bean Validation 3.1. Kotlin users need 2.2+, and GraalVM support targets version 24 or higher.
a performance leap: modularization and AOT
The biggest improvement in this release is invisible until you look under the hood: Spring Boot split its core into smaller modules.
modularization: unpacking the suitcase
The old spring-boot-autoconfigure module was a monster. It bundled configuration logic for nearly everything Spring supports, whether you used it or not. That meant unnecessary classpath scanning, bloated startup times, and IDE autocomplete polluted with irrelevant suggestions.
Spring Boot 4 shattered that artifact into focused, specialized modules that load only when needed. The payoff is concrete: startup is faster because there's less code to scan, memory consumption drops because you only load what you actually use, IDE suggestions get sharper because the visible classes match your dependencies, and GraalVM native image generation runs more efficiently with a leaner dependency graph.
AOT compilation and GraalVM
Spring Boot 4 targets GraalVM 24+ with a significantly improved AOT pipeline. Compilation is faster, the startup memory footprint is smaller, static analysis eliminates dead code more aggressively, and reflection hints are handled better.
The new @ConfigurationPropertiesSource annotation takes modularization further by letting the AOT processor focus only on the configuration properties your application actually declares.
null safety: the end of NullPointerException
Java's relationship with null has caused more production incidents than anyone wants to count. Spring Framework 7 adopts JSpecify as the null-safety standard across the entire portfolio, and it makes a real difference.
what is JSpecify?
JSpecify is a specification backed by Google, JetBrains, Meta, and Oracle. It gives you three annotations. @Nullable declares that a value might be absent, so you need to check before using it. @NonNull guarantees the value is present. @NullMarked flips the default for an entire package, making everything non-null unless you explicitly opt out with @Nullable.
the game changer: @NullMarked
You can mark a whole package as null-safe by adding a package-info.java file:
// src/main/java/com/yourcompany/service/package-info.java
@org.jspecify.annotations.NullMarked
package com.yourcompany.service;With that in place, IntelliJ IDEA 2025.3+ (which supports JSpecify natively) flags potential null dereferences as you type. No more waiting for a runtime NPE to discover the problem.
@NullMarked
package com.example.users;
public class UserService {
// The IDE will warn if you try to pass null
public User createUser(@NonNull String name, @Nullable String lastName) {
// name is never null - safe to use directly
String fullName = name.toUpperCase();
// lastName can be null - you must check
if (lastName != null) {
fullName += " " + lastName.toUpperCase();
}
return new User(fullName);
}
}The compiler and IDE now enforce null contracts before your code ever reaches production.
declarative HTTP clients: the feign killer
Service-to-service communication in Spring used to mean either verbose RestTemplate code or pulling in Spring Cloud OpenFeign as an external dependency. Spring Boot 4 fixes both problems.
goodbye RestTemplate
RestTemplate is officially deprecated in Spring Framework 7. It was verbose, hard to test, and predates modern Java features by a decade.
the new era: RestClient and @HttpExchange
RestClient replaces RestTemplate with a fluent, builder-style API. But the real shift is declarative HTTP clients built with @HttpExchange. You define an interface, and Spring generates the implementation.
package com.yourcompany.clients;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@HttpExchange("/api/products")
public interface ProductClient {
@GetExchange("/{id}")
Product getProduct(@PathVariable("id") String id);
@GetExchange
List<Product> listProducts();
@PostExchange
Product createProduct(@RequestBody Product product);
}@Configuration
public class ClientConfig {
@Bean
public ProductClient productClient(RestClient.Builder builder) {
RestClient restClient = builder
.baseUrl("https://api.example.com")
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return factory.createClient(ProductClient.class);
}
}The benefits stack up fast. You write an interface, not boilerplate. The interface is trivial to mock in tests. Types are enforced by the compiler. And you need zero external dependencies.
Spring Boot 4 also adds a clientType attribute that lets you switch between RestClient (default) and WebClient (for reactive scenarios) without touching client code.
resilience and API versioning
Two patterns that used to require external libraries are now baked into the framework.
native resilience
The annotations from Spring Retry have moved into Spring Framework 7 core. @Retryable retries a method on failure. @ConcurrencyLimit caps concurrent invocations. And @EnableResilientMethods activates both on your beans.
@Service
@EnableResilientMethods
public class PaymentService {
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public PaymentResponse processPayment(PaymentRequest request) {
// If it fails, it retries up to 3 times with 1 second wait
return paymentGateway.process(request);
}
@ConcurrencyLimit(maxConcurrency = 10)
public Report generateReport() {
// Maximum 10 concurrent calls
return reportService.generate();
}
}native API versioning
REST API versioning has been a pain point for years. Third-party libraries, custom headers, URL path conventions, everyone does it differently. Spring Framework 7 adds a version attribute directly to @RequestMapping and its variants.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// Version 1: Simple response
@GetMapping(version = "1", produces = MediaType.APPLICATION_JSON_VALUE)
public List<OrderV1> getOrdersV1() {
return orderService.getSimpleOrders();
}
// Version 2: Enriched response with metadata
@GetMapping(version = "2", produces = MediaType.APPLICATION_JSON_VALUE)
public OrderResponseV2 getOrdersV2() {
return orderService.getCompleteOrders();
}
}You define the versioning strategy once, centrally, and can switch between header-based, path-based, or query parameter approaches without rewriting controllers.
@Configuration
public class ApiVersioningConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// Header strategy
configurer.apiVersioning(builder -> builder
.strategy(VersionStrategy.HEADER)
.headerName("API-Version")
);
// Or by path: /v1/orders, /v2/orders
// Or by query parameter: /orders?version=1
}
}high-definition observability
Spring Boot 4 makes observability a first-class concern. Micrometer 2.0 ships with performance improvements, and Spring Boot 4 includes a native OpenTelemetry starter. Logs, metrics, and traces are automatically correlated, so you can follow a request across service boundaries without manual trace propagation.
the @Observed annotation
Slap @Observed on any method and you get execution time, call count, and error rate metrics for free:
@Service
public class InventoryService {
@Observed(name = "inventory.check", contextualName = "checkAvailability")
public boolean checkAvailability(String productId) {
// Automatically generates metrics for:
// - Execution time
// - Number of calls
// - Error rate
return inventoryRepository.hasStock(productId);
}
}SSL health monitoring
The Actuator now watches SSL certificate expiration. When a certificate is within 14 to 30 days of expiring, /actuator/health reports it explicitly:
{
"status": "UP",
"components": {
"ssl": {
"status": "WARNING",
"details": {
"certificate": "api.example.com",
"expiresIn": "12 days",
"expiryDate": "2025-12-05"
}
}
}
}No more midnight pages because a cert expired and nobody noticed.
core changes: the client pattern
Spring has used the Template Method pattern for years: JdbcTemplate, RestTemplate, JmsTemplate. Spring Framework 7 is replacing these with a Client pattern built on builders and functional interfaces.
RestTemplate gives way to RestClient and @HttpExchange (deprecated). JdbcTemplate and NamedParameterJdbcTemplate now coexist with JdbcClient. And JmsTemplate gets a new JmsClient companion.
JdbcClient: cleaner code
JdbcClient kills the verbosity. Instead of anonymous RowMapper classes, you get a fluent builder that reads like the query it executes.
Before, with JdbcTemplate:
public List<Person> findAll() {
return jdbcTemplate.query(
"SELECT id, name, age FROM person",
new RowMapper<Person>() {
@Override
public Person mapRow(ResultSet rs, int rowNum) throws SQLException {
Person p = new Person();
p.setId(rs.getLong("id"));
p.setName(rs.getString("name"));
p.setAge(rs.getInt("age"));
return p;
}
}
);
}Now, with JdbcClient:
public List<Person> findAll() {
return jdbcClient.sql("SELECT id, name, age FROM person")
.query((rs, rowNum) -> new Person(
rs.getLong("id"),
rs.getString("name"),
rs.getInt("age")
))
.list();
}
// Or even simpler with automatic mapping
public List<Person> findAll() {
return jdbcClient.sql("SELECT * FROM person")
.query(Person.class)
.list();
}Parameterized queries:
public Optional<Person> findById(Long id) {
return jdbcClient.sql("SELECT * FROM person WHERE id = :id")
.param("id", id)
.query(Person.class)
.optional();
}Write operations:
public int updateAge(Long id, int newAge) {
return jdbcClient.sql("UPDATE person SET age = :age WHERE id = :id")
.param("age", newAge)
.param("id", id)
.update();
}The new API is more concise, uses lambdas and method references naturally, integrates with Optional for safe null handling, and plays better with AOT and GraalVM.
testing improvements
more granular test slices
Spring Boot 4 adds test slices that load only the configuration needed for the component under test:
@HttpServiceClientTest
class ProductClientTest {
@Autowired
private ProductClient productClient;
@Test
void shouldGetProduct() {
// Only loads configuration needed for HTTP clients
Product product = productClient.getProduct("123");
assertThat(product).isNotNull();
}
}@HttpServiceClientTest spins up just enough context to test a declarative HTTP client, without loading the full application.
improved TestRestTemplate
A new TestRestClient integrates with the modern RestClient API and supports typed responses:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class IntegrationTest {
@Autowired
private TestRestClient restClient;
@Test
void shouldCreateUser() {
User newUser = new User("John", "Doe");
User created = restClient.post()
.uri("/api/users")
.body(newUser)
.exchange()
.expectStatus().isCreated()
.expectBody(User.class)
.returnResult();
assertThat(created.getId()).isNotNull();
}
}migration from Spring Boot 3
mandatory changes
You need to handle four things. Jackson jumps from 2.x to 3.x. The javax.* namespace is fully replaced by jakarta.*. Java 17 is the floor, though 21+ is recommended. And Kotlin needs to be 2.2+.
important deprecations
RestTemplate is on its way out, so start moving to RestClient or declarative clients. Lambda-based security configuration is now mandatory in Spring Security, so the old WebSecurityConfigurerAdapter style is gone. Several configuration properties have also been renamed.
migration tools
Spring recommends OpenRewrite to automate the mechanical parts:
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.42.0</version>
<configuration>
<activeRecipes>
<recipe>org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0</recipe>
</activeRecipes>
</configuration>
</plugin>The plugin handles dependency updates, namespace migration, deprecated API replacement, and configuration property renaming.
why migrate
Spring Boot 4 and Spring Framework 7 make Java applications lighter, safer, and faster to build. Modularization and AOT cut startup times and memory consumption. JSpecify catches null errors at compile time. Declarative HTTP clients and the Client pattern reduce boilerplate. Resilience patterns and observability are built in. And the whole stack is tuned for containers and cloud environments.
The migration isn't trivial. You'll update dependencies, fix namespaces, and adjust configurations. But the productivity and performance gains pay for the effort quickly. If you're building Java services for the next few years, this is the foundation to build on.