Appearance
RestClient in Spring Boot Microservices | Synchronous Communication using RestClient | Fluent API
The Assembly Line Analogy
Think of building a car on a factory floor. In the old approach, the supervisor had to memorise a separate checklist for every possible combination of model, color, and options — a massive binder full of overloaded procedures. The new approach uses a sequential assembly line: first the chassis, then the engine, then the paint, then the trim. The order is fixed and obvious; every worker knows exactly what comes next. You cannot apply paint before the engine is mounted — the line simply does not allow it.
RestClient is the assembly line. Each method you chain opens up the next set of operations. You can only move forward in the sequence; you can never jump to a stage that requires a previous stage to be complete first. This property — called a fluent API — eliminates the need to memorise dozens of overloaded method variants.
RestTemplate Limitations Recap
From the previous chapter, the major problems with RestTemplate were:
- Dozens of overloaded methods (
getForObject,getForEntity,postForObject,postForEntity,exchange× many overloads …). Hard to remember, hard to maintain. - Added before modern HTTP features — each new capability (retry, circuit breaker, interceptors) required even more overloads.
- Not extensible cleanly.
Alternatives: WebClient vs. RestClient
WebClient | RestClient | |
|---|---|---|
| Nature | Asynchronous, non blocking | Synchronous, blocking |
| Programming model | Reactive (Spring WebFlux) | Imperative (Spring MVC) |
| Introduced | Spring 5 | Spring Framework 6 / Spring Boot 3 |
| Use when | You want reactive pipelines | You want simple, readable HTTP calls |
For microservice communication that follows a request-response pattern and does not require a reactive stack, RestClient is the preferred modern choice.
Creating a RestClient
As a Spring Bean
java
@Configuration
public class AppConfig {
/**
* RestClient.create() is shorthand for RestClient.builder().build().
* A single shared instance is safe across threads.
*/
@Bean
public RestClient restClient() {
return RestClient.create();
}
/**
* RestClient with a preconfigured base URL.
* Relative URIs in .uri() are resolved against this base.
*/
@Bean
public RestClient productRestClient() {
return RestClient.builder()
.baseUrl("http://localhost:8082")
.build();
}
}Minimal GET Example
java
@RestController
@RequestMapping("/order")
public class OrderController {
private final RestClient restClient;
public OrderController(RestClient restClient) {
this.restClient = restClient;
}
@GetMapping("/{id}")
public String placeOrder(@PathVariable Long id) {
String response = restClient
.get() // choose HTTP method
.uri("http://localhost:8082/product/" + id) // set the target URL
.retrieve() // indicate intent to read the response
.body(String.class); // extract and deserialize body
System.out.println("Response from product service: " + response);
return "Order placed. " + response;
}
}Compare this with the RestTemplate equivalent that required knowing which of many overloads to use. Here the sequence .get() → .uri() → .retrieve() → .body() is always the same pattern regardless of whether you are doing a GET, POST, PUT, or DELETE.
What Is a Fluent API?
A fluent API is built from method chaining where each method returns an object that exposes only the operations that make sense at that stage. Consider:
restClient.get() → returns RequestHeaderUriSpec
.uri("…") → returns RequestHeadersSpec
.header("…","…") → returns RequestHeadersSpec (same stage, keeps adding headers)
.retrieve() → returns ResponseSpec
.body(String.class) → returns String (final result)Each arrow represents a class boundary. The method .uri() is defined on RequestHeaderUriSpec, not on ResponseSpec. So you cannot call .uri() after .retrieve() — the chain would break because ResponseSpec simply does not have that method. This enforced sequencing is what makes the API self-documenting and prevents incorrect usage.
Class Diagram Summary
RestClient
└─ get() / post() / put() / delete()
└─ RequestBodyUriSpec (implements RequestHeaderUriSpec + RequestBodySpec)
├─ .uri(String) → RequestBodySpec / RequestHeadersSpec
├─ .header(k, v) → same spec (chaining within stage)
├─ .accept(MediaType) → same spec
├─ .body(Object) → RequestHeadersSpec (POST/PUT only)
└─ .retrieve() → ResponseSpec
├─ .body(Class) → T
├─ .toEntity(Class) → ResponseEntity<T>
└─ .onStatus(…) → ResponseSpec (for exception handling)Why Sequence Matters
java
// WRONG — calling .accept() before .uri() blocks you from ever calling .uri() again
restClient.get()
.accept(MediaType.APPLICATION_JSON) // now stuck on RequestHeadersSpec
.uri("http://…") // COMPILE ERROR — uri() not available here
.retrieve()
.body(String.class);
// CORRECT
restClient.get()
.uri("http://…")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(String.class);POST, PUT, DELETE Examples
POST — Creating a Resource
java
Product newProduct = new Product("Laptop", 999.99);
ResponseEntity<Product> response = restClient
.post()
.uri("http://localhost:8082/product")
.contentType(MediaType.APPLICATION_JSON)
.body(newProduct) // serialize Java object → JSON automatically
.retrieve()
.toEntity(Product.class); // full ResponseEntity including status + headers
System.out.println("Created with status: " + response.getStatusCode());
System.out.println("Body: " + response.getBody());PUT — Updating a Resource
java
Product updated = new Product("Gaming Laptop", 1299.99);
restClient
.put()
.uri("http://localhost:8082/product/{id}", productId)
.contentType(MediaType.APPLICATION_JSON)
.body(updated)
.retrieve()
.toBodilessEntity(); // PUT typically returns no bodyDELETE
java
restClient
.delete()
.uri("http://localhost:8082/product/{id}", productId)
.retrieve()
.toBodilessEntity();Exception Handling with onStatus
In RestTemplate, exception handling was awkward and inconsistent. RestClient provides a clean onStatus hook on the ResponseSpec:
java
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return restClient
.get()
.uri("http://localhost:8082/product/" + id)
.retrieve()
.onStatus(status -> status.is4xxClientError(), (request, response) -> {
throw new ProductNotFoundException(
"Product not found. Status: " + response.getStatusCode()
);
})
.onStatus(status -> status.is5xxServerError(), (request, response) -> {
throw new ProductServiceException(
"Product service error. Status: " + response.getStatusCode()
);
})
.body(Product.class);
}How it works internally: onStatus stores the predicate and handler pair in a list on the ResponseSpec. When body() is called, it internally invokes the exchange method. The exchange method evaluates each registered status handler before attempting body deserialization. If a handler matches, it throws the provided exception, short-circuiting the deserialization step.
Using exchange Directly for Full Control
If you want to bypass onStatus and body() and control everything yourself, call exchange() directly:
java
String result = restClient
.get()
.uri("http://localhost:8082/product/1")
.exchange((request, response) -> {
if (response.getStatusCode().is4xxClientError()) {
throw new ProductNotFoundException("Not found");
}
if (response.getStatusCode().is5xxServerError()) {
throw new ProductServiceException("Server error");
}
// Manual deserialization from response body stream
return StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
});Use exchange when you need to inspect headers or status before deciding how to parse the body.
Adding Interceptors
Interceptors allow you to add cross cutting behavior (authentication headers, request logging, correlation IDs) at the RestClient level rather than at each call site.
java
@Component
public class AuthInterceptor implements ClientHttpRequestInterceptor {
private final TokenProvider tokenProvider;
public AuthInterceptor(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// Add auth header to every outgoing request
request.getHeaders().set("Authorization", "Bearer " + tokenProvider.currentToken());
// Log the request
System.out.println("Outgoing request to: " + request.getURI());
return execution.execute(request, body);
}
}java
@Bean
public RestClient restClientWithAuth(AuthInterceptor authInterceptor) {
return RestClient.builder()
.baseUrl("http://localhost:8082")
.requestInterceptor(authInterceptor)
.build();
}Interceptors are applied before the call to execute() inside the framework, so they run after the request is fully constructed but before the TCP connection is opened.
RestClient Internal Flow
RestClient uses a different HTTP client underneath compared to RestTemplate:
JdkClientHttpRequestFactory(default in Spring 6) creates anHttpClientfromjava.net.http— the modern Java HTTP client available since Java 11. This client supports HTTP/1.1 and HTTP/2 (concurrent streams over a single connection).execute()callsHttpClient.sendAsync()internally, but immediately blocks for the result (withCompletableFuture.join()). This is whyRestClientis synchronous from the caller's perspective even though the underlying I/O uses async APIs.Response handling — once the response is available, the registered
ResponseSpechandlers run. If no error handler matches, a registeredHttpMessageConverterdeserializes the response body to the target type.Connection management —
HttpClientmanages its own connection pool. Unlike RestTemplate'sKeepAliveCache, the modern client handles HTTP/2 multiplexing transparently.
HTTP/2 Support: A Key Advantage Over RestTemplate
RestTemplate (via HttpUrlConnection) supports only HTTP 1.0 and HTTP 1.1. With HTTP 1.1, each TCP connection handles one request at a time in sequence.
RestClient (via java.net.http.HttpClient) supports HTTP/2, which allows multiple concurrent HTTP requests to be multiplexed over a single TCP connection. The server can also send responses out of order. This results in significantly lower latency when a service makes many parallel downstream calls.
Interview Questions & Pitfalls
Q1. What is the difference between RestClient and WebClient?
Both are modern alternatives to RestTemplate. RestClient is synchronous and blocking — the calling thread waits for the response before proceeding. WebClient is asynchronous and non blocking and belongs to the Spring WebFlux reactive stack. Use RestClient in a standard Spring MVC application; use WebClient when you are building a reactive application or need non blocking I/O.
Q2. What is a fluent API and how does RestClient implement it?
A fluent API uses method chaining where each method returns an object that exposes only the next logical set of operations. RestClient achieves this through a chain of interfaces: get() returns RequestHeaderUriSpec, .uri() returns RequestBodySpec or RequestHeadersSpec, .retrieve() returns ResponseSpec, and .body() returns the deserialized type. Because each method returns a different interface type, the compiler enforces the correct order of operations.
Q3. What does retrieve() actually do? Does it make the HTTP call?
No. retrieve() creates a ResponseSpec object and stores the request configuration built so far. The actual HTTP call (TCP connection, request transmission, response reading) happens when you call .body(), .toEntity(), or .toBodilessEntity() on the ResponseSpec.
Q4. How do you add a global authentication header to all requests made by a RestClient?
Implement ClientHttpRequestInterceptor, set the header in the intercept method, then register the interceptor on the RestClient.Builder via .requestInterceptor(myInterceptor). Every request made through that RestClient instance will include the header automatically.
Q5. What is a common mistake when using onStatus with RestClient?
A common pitfall is forgetting that onStatus is evaluated only when .body() or .toEntity() is called, not when .retrieve() is called. Another pitfall is registering a handler for 4xx errors but not for 5xx errors — the default behavior for unhandled error status codes is to throw a generic RestClientException, which may mask the real cause.
Q6. How is exception handling in RestClient better than in RestTemplate?
RestTemplate required wrapping calls in try-catch for HttpClientErrorException and HttpServerErrorException, and custom error handling required subclassing DefaultResponseErrorHandler. RestClient provides a clean onStatus method directly on the call chain, allowing you to register type safe, expressive handlers per status range without subclassing framework classes.
Q7. What HTTP versions does RestClient support compared to RestTemplate?
RestTemplate (using HttpUrlConnection) supports HTTP 1.0 and HTTP 1.1. RestClient (using java.net.http.HttpClient by default) supports HTTP 1.1 and HTTP/2, including the multiplexing feature of HTTP/2 where multiple requests share a single TCP connection concurrently.