← Back to Blog

Four Spring Boot 4 Features That Actually Change Your Code

Spring Boot 4 release notes are long. Four features change how you actually write code: API versioning, HTTP service clients, virtual threads, RestTestClient.

Spring Boot 4 went GA. The release notes are long. Spring Framework 7, Jakarta EE 11, Jackson 3 (with Jackson 2 shipping deprecated), Hibernate 7.1, Gradle 9 support. Most of it is plumbing you will never touch. Some of it deprecates code you wrote last year. Four features actually change how you write a Spring Boot application day to day. Those are the ones worth your time.

I will cover them in the order you will notice them.

API Versioning Is Now First-Class

I have shipped at least three versions of this code by hand. URL prefix routing (/v1, /v2). A custom RequestMappingHandlerMapping that reads a header. A servlet filter that rewrites the request path. None of these are hard, but every team writes them slightly differently, and once you have two of them in one service you start dropping requests because the version resolution order is wrong.

Spring Boot 4 ships API versioning as an auto-configured feature. You pick the resolution strategy in application.properties:

spring.mvc.apiversion.use.header=X-API-Version
spring.mvc.apiversion.supported=1,2,3
spring.mvc.apiversion.default=1

Then your controllers declare the version directly on the mapping:

@GetMapping(path = "/orders/{id}", version = "1")
public OrderV1 getV1(@PathVariable Long id) { ... }

@GetMapping(path = "/orders/{id}", version = "2+")
public OrderV2 getV2(@PathVariable Long id) { ... }

Three things matter here. Version is part of the mapping signature, so two controllers can share the same path without colliding. The 2+ syntax matches v2 and above without making you list each version explicitly. And you can deprecate a version with a Deprecation response header by registering an ApiVersionDeprecationHandler bean.

If you want path-based versioning (/v1/orders) instead of headers, set spring.mvc.apiversion.use.path-segment=0 and the controller syntax is identical. The strategy is configurable. The controllers do not change.

For WebFlux, the same properties are mirrored under spring.webflux.apiversion.*. The custom hooks (ApiVersionResolver, ApiVersionParser, ApiVersionDeprecationHandler) are framework-level beans that work in both stacks.

This is the kind of feature you would write yourself in two days. Now you do not have to.

HTTP Service Clients Are Auto-Configured

Spring 6 introduced @HttpExchange, the declarative HTTP client interface. The idea: write a Java interface, annotate the methods, and Spring generates the implementation. Like Feign without the Netflix baggage.

The catch in Spring Boot 3 was the wiring. You had to create a WebClient or RestClient bean, build an HttpServiceProxyFactory, and call createClient(YourInterface.class). Every external service got its own *ClientConfig.java with the same five-line dance. Tedious.

Spring Boot 4 auto-configures it. You declare an interface:

@HttpExchange("https://api.example.com")
public interface PaymentClient {

    @GetExchange("/charges/{id}")
    Charge getCharge(@PathVariable String id);

    @PostExchange("/charges")
    Charge create(@RequestBody ChargeRequest request);
}

Register it once:

@SpringBootApplication
@ImportHttpServices(types = PaymentClient.class)
public class Application { ... }

Then inject it like any other bean. Base URL, timeouts, default headers, and the underlying transport (RestClient, WebClient, or RestTemplate) are configurable via properties under spring.http.client.service.*. Per-client overrides use a group key, so a payments client and an inventory client can have different timeouts without writing code.

In a service with five external integrations, this deletes five config classes. It is not a feature you will blog about a year from now, because you will have forgotten it was ever your problem.

Virtual Threads Now Reach Your HTTP Clients

The flag spring.threads.virtual.enabled=true has worked since Spring Boot 3.2. It puts the request thread (Tomcat or Jetty) on a virtual thread. That is the big-ticket item, and most people stop there.

Until 4.0, outbound HTTP calls were a different story. If you used WebClient, you were already non-blocking, so virtual threads were irrelevant. If you used RestTemplate or RestClient over JdkClientHttpRequestFactory, the underlying java.net.http.HttpClient ran its own fixed-size executor for response delivery and a separate selector thread. A virtual thread making a blocking outbound call could still end up parking a carrier thread while it waited for the response delivery executor.

Spring Boot 4 wires the auto-configured JdkClientHttpRequestFactory and the JDK HttpClient instance behind it to use virtual threading when the flag is on. The whole request, inbound to outbound, lives on virtual threads. No manual HttpClient.Builder.executor(...) required.

This is the kind of fix that matters at the tail. Under steady load you will not notice. Under a thread-pool starvation event during a downstream slowdown, you will.

RestTestClient Is the Test Client You Actually Wanted

Web layer testing in Spring Boot has been a choice between three wrong shapes.

MockMvc: no real HTTP, awkward fluent API, does not catch HTTP-layer mistakes like a misconfigured filter or wrong content negotiation. Works well for controllers in isolation, badly for anything that involves the servlet pipeline.

TestRestTemplate: real HTTP, but the API predates fluent assertions. You end up writing helper methods just to make the tests readable.

Then there's WebTestClient: clean fluent API, but pulls spring-webflux onto the test classpath even when your application is pure MVC. Most teams accept it. It still feels wrong.

Spring Boot 4 adds RestTestClient. Same fluent API as WebTestClient, without the reactive dependency, and it runs in two modes against the same test body.

Slice test mode (no server, fast):

@WebMvcTest(OrderController.class)
@AutoConfigureMockMvc
class OrderControllerTest {

    @Autowired
    RestTestClient client;

    @Test
    void getsOrder() {
        client.get().uri("/orders/{id}", 42)
            .exchange()
            .expectStatus().isOk()
            .expectBody(Order.class)
            .value(o -> assertThat(o.id()).isEqualTo(42));
    }
}

Integration test mode (real server, slow):

@SpringBootTest(webEnvironment = RANDOM_PORT)
class OrderControllerIT {

    @Autowired
    RestTestClient client;

    // identical test body
}

The test body does not change when you promote a slice test to an integration test. That is the practical win. Today, when you decide you actually need to hit the running server, you rewrite your assertions because you are switching client libraries. With RestTestClient you change the test class annotation and nothing else.

Two Things to Know Before You Upgrade

Jackson 3 is the new default. Jackson 2 ships in deprecated form. Your existing imports still work. Your ObjectMapper configuration still works. But the com.fasterxml.jackson packages have moved to tools.jackson in v3, and a future Spring Boot release will remove the v2 shim entirely. Migrate before you have to. The JsonMapper.builder() entry point in v3 is the one to standardize on.

Jakarta EE 11 means jakarta.servlet 6.1. If you still have any javax.servlet imports somewhere in the codebase, they will not compile. The fix is mechanical (find-and-replace), but it is the kind of compile error that surprises people who have not touched a particular module in two years.

What This Changes

Spring Boot release notes are long because every Spring module gets a section. Most of those sections are plumbing. The four features above are the ones that affect what you type on a Tuesday afternoon when you are adding a new endpoint or a new client.

API versioning replaces a class of bespoke routing code. HTTP service clients delete a class of config files. With virtual threads now reaching outbound HTTP, the last gap in the thread-per-request model closes. And RestTestClient consolidates three test clients into one.

The rest is real, but it is the kind of thing you will discover when you go looking. These four are the ones you will notice immediately.

Share
X LinkedIn HN
UI

Umur Inan

Principal Software Engineer

Backend engineer focused on JVM systems, distributed architecture, and the failure modes that only show up in production. I write about what I learn building and breaking things at scale.

👁 0 5 min read

Comments (0)