RestClient in Spring Boot – Modern Alternative to RestTemplate

(Deep Dive into Fluent API, Internals & Interceptors)
In the previous part, we understood why RestTemplate is no longer the best choice for modern applications.
Let’s quickly recall its limitations:
- Too many overloaded methods → hard to remember
- Designed before Retry / Circuit Breaker concepts
- Not user friendly for extension
- In maintenance mode (no new features)
So Spring introduced two modern alternatives:
| Client | Nature |
|---|---|
| WebClient | Asynchronous / Non-blocking |
| RestClient | Synchronous / Blocking |
In this blog, we focus on RestClient.
What is RestClient?
RestClient is a modern, fluent, builder-style HTTP client introduced in:
- Spring Framework 6.0+
- Spring Boot 3.0+
It is:
- Synchronous (blocking)
- Readable
- Extensible
- Future-ready
Architecture Example
We continue with the same setup:
| Service | Port |
|---|---|
| OrderService | 8081 |
| ProductService | 8082 |
OrderService needs to call ProductService.



What is Fluent API?
Fluent API means method chaining.
Each method:
- Returns the next stage object
- Exposes only valid next operations
Example:
Product product = restClient.get()
.uri("http://localhost:8082/products/1")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(Product.class);
Readable like English.
Core Interfaces in RestClient
Understanding these explains everything:
Entry Points
RequestHeadersUriSpec<?> get();
RequestBodyUriSpec post();
RequestBodyUriSpec put();
RequestHeadersUriSpec<?> delete();
URI Stage
UriSpec<S extends RequestHeadersSpec<?>>
S uri(String uri, Object... vars);
Headers Stage
RequestHeadersSpec<S>
S accept(MediaType...);
S header(String name, String... values);
ResponseSpec retrieve();
Body Stage (POST/PUT)
RequestBodySpec
RequestBodySpec body(Object body);
Response Stage
ResponseSpec
<T> T body(Class<T> type);
<T> ResponseEntity<T> toEntity(Class<T> type);
ResponseEntity<Void> toBodilessEntity();
Why Order of Calls Matters
This fails:
restClient.get()
.accept(MediaType.APPLICATION_JSON)
.uri("http://localhost:8082/products/1"); // ❌ Not allowed
Why?
Because:
accept()returnsRequestHeadersSpec- That interface does not contain
uri() - Generics restrict next method
So sequence matters.
GET Flow Internally
restClient.get()
.uri(...)
.accept(...)
.header(...)
.retrieve()
.body(Product.class);
Internally:
- Creates
DefaultRequestBodyUriSpec - Sets HttpMethod = GET
- Sets URI
- Adds headers
- Creates
DefaultResponseSpec - Calls
exchange() - Creates TCP connection
- Sends HTTP request
- Maps response body
POST Flow
restClient.post()
.uri("/products")
.body(product)
.retrieve()
.toEntity(Product.class);
Now return type is RequestBodySpec, so:
- You can call
body() - You can call headers
- Then retrieve
DELETE Flow
restClient.delete()
.uri("/products/1")
.retrieve()
.toBodilessEntity();
No body expected.
How Response is Handled
Handled by:
DefaultResponseSpec
This class:
- Knows how to convert body
- Knows how to map headers
- Knows how to throw exceptions
Exception Handling
restClient.get()
.uri("/products/99")
.retrieve()
.onStatus(
status -> status.is4xxClientError(),
(req, res) -> new RuntimeException("Product not found")
)
.body(Product.class);
You define:
- Which status is error
- How to handle it
What Happens in exchange()?
This is where real magic happens.
Internally:
JdkClientHttpRequestFactory
↓
HttpClient (java.net.http)
↓
sendAsync()
↓
TCP connection
↓
HTTP request
↓
HTTP response
Features of new HTTP client:
- Connection pooling
- HTTP/1.1 + HTTP/2
- Async internally
- Better performance
RestClient vs RestTemplate Internals
| Feature | RestTemplate | RestClient |
|---|---|---|
| HTTP client | HttpURLConnection | java.net.http.HttpClient |
| Protocol | HTTP/1.1 | HTTP/1.1 + HTTP/2 |
| Connection pool | Basic | Advanced |
| Retry ready | No | Yes |
| Circuit breaker | No | Yes |
| Interceptors | Hard | Easy |
Adding Interceptors
Interceptors allow:
- Logging
- Authentication
- Tracing
- Metrics
Example:
RestClient client = RestClient.builder()
.requestInterceptor((req, body, exec) -> {
req.getHeaders().add("X-Source", "OrderService");
return exec.execute(req, body);
})
.build();
Now ProductService receives:
X-Source: OrderService
How Interceptor Works Internally
At exchange step:
RestClient.execute()
↓
InterceptingClientHttpRequest
↓
Your interceptor
↓
Real HTTP call
Interceptor wraps the actual execution.
When to Use exchange() Directly?
If you want:
- Full control over request
- Custom serialization
- Custom error handling
Then skip retrieve():
restClient.get()
.uri("/products/1")
.exchange((req, res) -> {
if (res.getStatusCode().is2xxSuccessful()) {
return new ObjectMapper()
.readValue(res.getBody(), Product.class);
}
throw new RuntimeException("Error");
}, true);
RestClient vs WebClient
| RestClient | WebClient |
|---|---|
| Blocking | Non-blocking |
| Simpler | Reactive |
| MVC friendly | WebFlux |
| Easier learning | Steep learning |
Interview Questions
Q1. What is RestClient?
Modern synchronous HTTP client introduced in Spring 6.
Q2. Why RestTemplate is deprecated?
Not extensible, outdated, no support for modern resilience patterns.
Q3. What is Fluent API?
Method chaining where each step exposes only valid next methods.
Q4. Why order of method calls matters?
Because generics restrict which methods are visible.
Q5. Which HTTP client RestClient uses?
java.net.http.HttpClient (JDK 11+).
Q6. How to add interceptors?
Using RestClient.builder().requestInterceptor().
Final Thoughts
RestClient represents Spring’s final evolution of synchronous HTTP:
- Clean API
- Strong typing
- Better internals
- Resilience friendly
Real-world Recommendation:
| Use Case | Tool |
|---|---|
| Legacy | RestTemplate |
| Modern MVC | RestClient |
| Reactive systems | WebClient |
| Large microservices | Feign + Resilience4j |
If you’re building Spring Boot 3+ applications,
👉 RestClient should be your default choice.