Zademy

RestClient in Spring Boot 4: The Modernization of Synchronous Communication

spring-boot; spring-framework; restclient; http-client
words words

Spring Boot 4.0 ships on top of Spring Framework 7, and it changes how Java apps do HTTP. The short version: RestTemplate is on borrowed time, RestClient is the new default for synchronous calls, and virtual threads make blocking code scale like reactive without the cognitive overhead.

What RestClient brings to the table

Spring introduced RestClient in Framework 6.1 (Boot 3.2) as the successor to RestTemplate. By Boot 4, it is the standard for synchronous HTTP.

The API is fluent. You chain method calls to build the request, which reads a lot like WebClient if you have used the reactive side. Error handling is built in through onStatus(), so you branch on specific HTTP status codes instead of catching generic exceptions. Headers, query parameters, and URI variables go directly on the builder, no separate HttpEntity wrapper needed. And you get interceptors for cross-cutting concerns like logging or auth tokens.

Declarative HTTP clients

The bigger story in Boot 4 is native support for declarative HTTP interfaces. You write a plain Java interface with @HttpExchange annotations, and Spring generates the client proxy at startup. No manual RestClient calls, no HttpServiceProxyFactory boilerplate. Spring Framework 7 adds @ImportHttpServices, and Boot 4's auto-configuration wires everything as Spring beans. People are calling this the "Feign killer," and I think that is fair.

Virtual threads change the game

Here is the part that actually matters for performance. RestClient is synchronous and blocking. In the old world, that meant each in-flight request held a platform thread, and platform threads are expensive (1 to 8 MB of stack each). You scaled by adding thread pool size until you ran out of memory.

With virtual threads from Java 21+, that calculation flips. When a virtual thread hits an I/O call like an HTTP request, it parks and yields the underlying platform thread to another task. The carrier thread stays busy doing useful work instead of sleeping on a socket. Virtual threads weigh roughly 1 KB each, so you can have millions of them without breaking a sweat.

The practical result: Spring MVC apps using RestClient with virtual threads handle I/O-bound workloads at scale comparable to reactive WebClient, but with straightforward blocking code. No Mono, no Flux, no operator chains to debug.

Flip spring.threads.virtual.enabled to true and Boot 4 configures everything for you. Timeouts are centralized through spring.http.clients.connect-timeout and spring.http.clients.read-timeout.

RestClient vs RestTemplate

The API difference is night and day. RestTemplate uses the template pattern with dozens of overloaded methods. RestClient uses a fluent builder chain.

RestTemplate has been deprecated since Framework 6.1 and is scheduled for removal in Framework 8.0. RestClient is the de facto standard.

Creating a client goes from new RestTemplate() to RestClient.create(). A GET request goes from restTemplate.getForObject(url, String.class) to restClient.get().uri(url).retrieve().body(String.class). A POST goes from restTemplate.postForEntity(url, request, T.class) to restClient.post().uri(url).body(request).retrieve().toEntity(T.class).

The fluent version is longer in character count, but it reads top to bottom and every step is explicit. No more guessing which of 14 overloads does what you need.

Examples

Configured bean creation

Inject RestClient.Builder and configure a bean with your base URL and default headers:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfiguration {

    @Bean
    public RestClient githubRestClient(RestClient.Builder builder) {
        // Configure a base URL and default header for all requests
        return builder
            .baseUrl("https://api.github.com")
            .defaultHeader("Accept", "application/json")
            .build();
    }
}

GET with path variables and query parameters

// Assuming 'githubRestClient' is already injected

public String getRepositoryInfo(String owner, String repo, boolean verbose) {
    String uriTemplate = "/repos/{owner}/{repo}";

    return githubRestClient.get()
        .uri(uriTemplate, owner, repo) // Maps {owner} and {repo}
        .queryParam("verbose", verbose) // Adds ?verbose=true or false
        .retrieve()
        .body(String.class);
}

POST (resource creation)

The client handles JSON serialization and deserialization automatically:

import org.springframework.http.MediaType;

public User createNewUser(NewUserDTO userData) {
    return githubRestClient.post()
        .uri("/users")
        .contentType(MediaType.APPLICATION_JSON) // Sets Content-Type
        .body(userData) // Object that will be serialized to JSON
        .retrieve()
        .body(User.class); // Deserializes response to a User object
}

Error handling by status code

Handle specific HTTP statuses before the client throws a generic exception:

import org.springframework.http.HttpStatus;

public User safeGetUser(long id) {
    return githubRestClient.get()
        .uri("/users/{id}", id)
        .retrieve()
        .onStatus(HttpStatus.NOT_FOUND, (request, response) -> {
            // Specific handling for 404
            System.out.println("User not found, returning default");
            // Can throw an exception or, in this case, return a default value
            throw new CustomResourceNotFoundException("User ID " + id + " not found");
        })
        .onStatus(HttpStatus::is5xxServerError, (request, response) -> {
            // Handling for 5xx (server errors)
            throw new RuntimeException("Internal service error.");
        })
        .body(User.class);
}

Declarative HTTP client (Spring Boot 4)

This is the pattern I would use for all new service-to-service clients. Define the contract as an interface, let Spring generate the implementation:

// 1. Client Interface (Contract Definition)
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;

@HttpExchange(url = "https://api.external.com/api/v1") // Base URL
public interface ExternalApi {
    @GetExchange("/items/{itemId}") // Maps to GET https://api.external.com/api/v1/items/{itemId}
    Item getItem(@PathVariable String itemId);
}

// 2. Usage in a Service (Spring Boot 4.0 injects the proxy automatically after configuring @ImportHttpServices)
@Service
public class ItemService {
    private final ExternalApi externalApi;

    // The generated proxy implementation is injected
    public ItemService(ExternalApi externalApi) {
        this.externalApi = externalApi;
    }

    public Item findItem(String id) {
        // Feels like calling a local method
        return externalApi.getItem(id);
    }
}

RestTemplate is leaving in Framework 8.0. RestClient is designed for virtual threads. If you are writing new Spring MVC code, use RestClient and adopt @HttpExchange interfaces for service-to-service calls. You get cleaner code, better scalability, and a migration path that does not require learning reactive programming.