RestClient in Spring Boot: Synchronous Communication
Learn how OrderService can call ProductService using the modern, fluent RestClient API, with practical Java examples and request/response handling.
RestClient
Synchronous Communication
Call another service using a readable, fluent HTTP client API in Spring applications.
What You'll Learn
- 1. Introduction to RestClient
- 2. Limitations of RestTemplate
- 3. Setting Up Two Microservices
- 4. What Is a Fluent API?
- 5. GET Request
- 6. POST Request
- 7. DELETE Request
- 8. Exception Handling with retrieve()
- 9. Using exchange()
- 10. Adding Request Interceptors
- 11. Best Practices and Summary
- 12. Interview Questions
- 13. Frequently Asked Questions
- 14. Related Articles
1. Introduction to RestClient
RestClient is a synchronous HTTP client introduced in Spring Framework 6.1. It provides a modern fluent API for making HTTP requests from Spring applications. The supplied notes describe it as the alternative to the older RestTemplate API.
In this example, OrderService calls ProductService. The client sends an HTTP request and waits for the target service's response before continuing. This is synchronous, or blocking, communication.
2. Limitations of RestTemplate
The supplied PDF lists these limitations of RestTemplate:
Why consider RestClient?
- RestTemplate has many overloaded methods, which can be harder to remember and maintain.
- It was designed before patterns and tools such as retry and circuit breakers became common, so extending usage can feel less convenient.
- RestTemplate is in maintenance mode: it receives bug fixes, but it is not the focus for new features.
RestClient provides a more fluent, readable way to express request steps. It is still synchronous and blocking; it is not a reactive or non-blocking client.
3. Setting Up Two Microservices
For the example, run two Spring Boot applications on different ports. OrderService is the calling service, and ProductService exposes a product endpoint.
OrderService — application.properties
server.port=8081ProductService — application.properties
server.port=8082ProductService controller
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}")
public String getProduct(@PathVariable String id) {
return "Product fetched with id: " + id;
}
}Configure a RestClient bean
@Configuration
public class AppConfig {
@Bean
public RestClient restClientInstance() {
return RestClient.create();
}
}Inject this bean into OrderService and use it to call the ProductService endpoint.
4. What Is a Fluent API?
A fluent API allows method calls to be chained together. Each step returns an object that exposes the operations available at the next step. In RestClient, a request often follows this sequence:
The sequence matters because each method represents a different stage of preparing or handling the HTTP request.
5. GET Request Using RestClient
Use GET when OrderService needs to read data from ProductService. The example below requests a product by ID and reads the response as a String.
RestClient restClient = RestClient.create();
String response = restClient
.get()
.uri("http://localhost:8082/products/" + id)
.accept(MediaType.APPLICATION_JSON)
.header("X-Custom-Header", "xyz")
.retrieve()
.body(String.class);accept() sets the media type the client can accept, and header() adds a request header. retrieve() starts response handling, while body(String.class) converts the response body to a String.
6. POST Request Using RestClient
Use POST when the client needs to send data to the target service, for example to create a product. The supplied notes use a ProductEntity object as the request body.
RestClient restClient = RestClient.create();
ResponseEntity<ProductEntity> response = restClient
.post()
.uri("http://localhost:8082/products/create")
.accept(MediaType.APPLICATION_JSON)
.header("Content-Type", "application/json")
.body(new ProductEntity("Ice-cream"))
.retrieve()
.toEntity(ProductEntity.class);
ProductEntity responseBody = response.getBody();body() supplies the object to send. toEntity() returns a ResponseEntity containing response details and the converted body.
7. DELETE Request Using RestClient
Use DELETE to request removal of a resource. The example retrieves the status information without expecting a response body.
RestClient restClient = RestClient.create();
ResponseEntity<Void> response = restClient
.delete()
.uri("http://localhost:8082/products/" + id)
.retrieve()
.toBodilessEntity();
HttpStatusCode deletionStatus = response.getStatusCode();toBodilessEntity() is useful when the response status and headers matter but the body does not.
8. Exception Handling with retrieve()
The supplied notes demonstrate customizing status handling with onStatus(). This allows the application to react differently to client errors (4xx) and server errors (5xx).
String response = restClient
.get()
.uri("http://localhost:8082/products/" + id)
.retrieve()
.onStatus(
status -> status.is4xxClientError(),
(request, clientResponse) -> {
throw new MyCustomException("Invalid request passed");
}
)
.body(String.class);Important idea
Use onStatus() when you want to define how selected HTTP response statuses should be handled while using retrieve(). The exception classes in the example are application-specific and must exist in your project.
9. Using exchange() for More Control
The PDF explains that exchange() can be used when you want direct control over response processing and exception handling instead of relying on the usual retrieve() response flow.
String responseBody = restClient
.get()
.uri("http://localhost:8082/products/" + id)
.exchange((request, response) -> {
if (response.getStatusCode().is4xxClientError()) {
throw new MyCustomException("Invalid request passed");
} else if (response.getStatusCode().is5xxServerError()) {
throw new IllegalStateException("Something went wrong at server");
}
return StreamUtils.copyToString(
response.getBody(),
StandardCharsets.UTF_8
);
});With exchange(), your callback receives the request and response, so you can inspect the status and decide how to map the result. Make sure your project has the required imports and custom exception classes.
10. Adding Request Interceptors
An interceptor can inspect or modify a request before it is sent. In the supplied example, the interceptor adds an X-Custom-Header header to the outgoing request.
Custom interceptor
public class MyCustomRequestInterceptor
implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution
) throws IOException {
request.getHeaders().add("X-Custom-Header", "myvalue");
return execution.execute(request, body);
}
}Register the interceptor
@Configuration
public class AppConfig {
@Bean
public ClientHttpRequestInterceptor customRequestInterceptor() {
return new MyCustomRequestInterceptor();
}
@Bean
public RestClient restClientInstance(
ClientHttpRequestInterceptor customRequestInterceptor) {
return RestClient.builder()
.requestInterceptor(customRequestInterceptor)
.build();
}
}When ProductService receives the request, its headers include X-Custom-Header: myvalue. The interceptor runs as part of request execution.
11. Best Practices and Summary
Remember these points
- Use RestClient for synchronous HTTP calls in Spring applications.
- Use the fluent chain to make request setup easy to read.
- Choose GET, POST and DELETE according to the operation you need.
- Use
retrieve()for the common response-handling flow. - Use
onStatus()to customize handling of selected HTTP statuses. - Use
exchange()when you need direct control over the response. - Register an interceptor when requests need shared headers or other common processing.
| Method | Typical purpose | Example in this article |
|---|---|---|
get() | Read a resource | Fetch product by ID |
post() | Send data / create a resource | Send ProductEntity |
delete() | Request resource deletion | Delete product by ID |
retrieve() | Use the standard response-spec flow | Read body or response entity |
exchange() | Handle request and response directly | Custom status handling |
requestInterceptor() | Apply common request logic | Add a custom header |
12. RestClient Interview Questions
13. Frequently Asked Questions
Yes. RestClient is synchronous; the caller waits for the response.
RestClient was introduced in Spring Framework 6.1. Spring Boot 3.2 and later versions align with Spring Framework 6.1 or newer.
Yes. You can set headers for an individual request or use a request interceptor for common headers.
Use exchange() when you need direct control over the response and custom response mapping or error handling.
Conclusion
RestClient gives Spring developers a modern fluent API for synchronous HTTP communication. By understanding request chaining, response handling, status checks, exchange() and interceptors, you can build clearer service-to-service calls between applications such as OrderService and ProductService.