In modern microservices, non-blocking asynchronous communication is critical to building scalable and resilient systems. Spring WebClient, part of Spring WebFlux, makes it easy to perform asynchronous HTTP calls, replacing the older, blocking RestTemplate.
This blog covers how to use WebClient for asynchronous communication, including examples, chaining, parallel calls, and error handling.
What is WebClient?
WebClient is a reactive, non-blocking HTTP client. Unlike RestTemplate, it doesnβt block the thread while waiting for a response. Instead, it returns reactive types:
Mono β Represents 0 or 1 element.
Flux β Represents 0 or N elements.
This enables high throughput and efficient use of resources, especially when calling multiple microservices concurrently.
Commonly Used Methods in Mono and Flux
1. flatMap β Chain Service Calls
flatMap is used to call another asynchronous method and flatten the result into the same pipeline. This is the most important method when chaining multiple WebClient calls.
2. map β Transform Response
map is used to transform the value inside a Mono or Flux synchronously.
3. zip β Combine Multiple Responses
zip is used when responses from multiple services need to be combined.
4. onErrorResume β Handle Failures Gracefully
onErrorResume helps in applying fallback logic when a failure occurs. Often used for resilience patterns, like returning fallback responses or calling a backup service.
5. doOnNext β Logging and Debugging
doOnNext executes side-effects whenever a new element is emitted, without modifying the actual data.
Benefits of Using Mono & Flux
1. Non-blocking: Your app can handle many requests without blocking threads.
2. Backpressure support: Consumers can control the rate at which data is emitted.
3. Composability: Easy to chain operations like map(), filter(), flatMap().
4. Error handling: Clean handling of errors without try/catch blocks.
5. Streaming: Ideal for reactive APIs, WebSockets, or real-time data.
Blocking vs. Non-blocking API Calls
Blocking API Calls:
The caller waits for the response before moving on.
The thread that makes the call is blocked until the operation completes.
Easy to understand and implement but not scalable for high-load systems.
@GetMapping("/blocking/product/{id}")
public ProductResponse getProductBlocking(@PathVariable Long id) {
return WebClient.create("http://localhost:8081/products")
.get()
.uri("/{id}", id)
.retrieve()
.bodyToMono(ProductResponse.class)
.block();
}
Non-blocking API Calls:
The caller does not wait, it continues execution immediately.
The response is handled asynchronously when it arrives.
More scalable, efficient, and ideal for reactive programming.
@GetMapping("/nonblocking/product/{id}")
public Mono<ProductResponse> getProductNonBlocking(@PathVariable Long id) {
return WebClient.create("http://localhost:8081/products")
.get()
.uri("/{id}", id)
.retrieve()
.bodyToMono(ProductResponse.class);
}
HTTP Methods in WebClient
1. GET Method:
GET API to fetches either a collection of resources or a singular resource.
Example 1: Use Mono to fetch only a single resource.
public Mono<ProductResponse> getProductById(Long productId) {
return productClient.get()
.uri("/{id}", productId)
.retrieve()
.bodyToMono(ProductResponse.class);
}
Example 2: Use Flux to fetch a collection of resources.
public Flux<ProductResponse> getAllProducts() {
return productClient.get()
.uri("/")
.retrieve()
.bodyToFlux(ProductResponse.class);
}
2. POST Method:
Use to send data to the server to create a new resource.
public Mono<ProductResponse> createProduct(ProductRequest productRequest) {
return productClient.post()
.uri("/create")
.bodyValue(productRequest)
.retrieve()
.bodyToMono(ProductResponse.class);
}
3. PUT Method:
Use to replace an existing resource.
public Mono<ProductResponse> updateProduct(Long productId, ProductRequest productRequest) {
return productClient.put()
.uri("/update/{id}", productId)
.bodyValue(productRequest)
.retrieve()
.bodyToMono(ProductResponse.class);
}
4. DELETE Method:
Use to delete a resource.
public Mono<Void> deleteProduct(Long productId) {
return productClient.delete()
.uri("/delete/{id}", productId)
.retrieve()
.bodyToMono(Void.class);
}
5. Patch Method:
Use to update only part of a resource.
public Mono<ProductResponse> reduceStock(Long productId, int quantity) {
return productClient.patch()
.uri(uriBuilder -> uriBuilder
.path("/{id}/reduceStock")
.queryParam("stock", quantity)
.build(productId))
.retrieve()
.bodyToMono(ProductResponse.class);
}
Chaining, Composition, and Parallel Calls
One of the most powerful aspects of reactive programming in Spring WebFlux is the ability to chain operations, compose streams, and execute API calls in parallel.
1. Chaining: Sequential, dependent operations.
Chaining allows to apply multiple operations sequentially on a reactive stream Transformations, filters, or side effects can be performed without blocking the thread.
public Mono<String> chainingProductAndCustomer(Long productId, Long customerId) {
return getCustomerById(customerId)
.flatMap(customer ->
getProductById(productId)
.map(product ->
"Customer: " + customer.getCustomerName() +
", Product: " + product.getProductName()
)
);
}
Explanation:
β’ getCustomerById() fetches a customer.
β’ Only after customer is fetched, getProductById() is called.
β’ Result combines customer and product info.
2. Composition: Dependent operations composed together.
Composition is about combining multiple reactive streams into one. This is particularly useful when making multiple API calls that either depend on each other or need to be merged.
public Mono<String> compositionExample(Long orderId) {
return getOrderById(orderId)
.flatMap(order -> getProductById(order.getProductId())
.flatMap(product -> getCustomerById(order.getCustomerId())
.map(customer ->
"Order: " + order.getOrderId() +
", Customer: " + customer.getCustomerName() +
", Product: " + product.getProductName()
)
)
);
}
Explanation:
β’ You fetch order, then based on order details, fetch product and customer.
β’ Uses nested flatMap, combining multiple dependent calls.
3. Flux Parallel Calls: Faster than chaining when calls donβt depend on each other.
When working with collections of IDs or items, Flux provides a convenient way to perform multiple parallel requests and merge their results.
public Mono<String> parallelExample(Long customerId, Long productId) {
Mono<String> customerMono = getCustomerById(customerId).map(c -> "Customer: " + c.getCustomerName());
Mono<String> productMono = getProductById(productId).map(p -> "Product: " + p.getProductName());
return Mono.zip(customerMono, productMono)
.map(tuple -> tuple.getT1() + " | " + tuple.getT2());
}
Explanation:
β’ getCustomerById() and getProductById() are independent β run in parallel.
β’ Mono.zip() combines results when both complete.
Exception Handling
In asynchronous communication with WebClient, exceptions are not managed using traditional try-catch blocks. Instead, they flow through the reactive pipeline (Mono or Flux). Operators such as onStatus, onErrorResume, onErrorReturn, and doOnError are used to handle them in a non-blocking manner.
1. Handling HTTP Error Status with onStatus()
When using retrieve(), WebClient automatically maps 2xx responses to the response body. Error statuses such as 4xx and 5xx require explicit handling through onStatus.
onStatus checks for error codes. Instead of returning a normal response, it maps them to a custom exception.
2. Fallback Mechanisms
Instead of failing the entire pipeline, a fallback strategy can be applied.
The operator onErrorReturn provides a static default response, while onErrorResume allows dynamic recovery, such as calling another service or applying conditional logic.
3. Timeout and Retry
Resilience in distributed systems often requires timeouts and retries. WebClient supports this directly in the reactive pipeline.
4. Centralized Error Handling
For consistent behavior across the application, exceptions can be handled globally using @ControllerAdvice.
Example of Asynchronous Communication
To demonstrate how reactive and asynchronous communication works in microservices, letβs build a simple e-commerce system using WebClient in Spring Boot.
The application consists of three services β Product Service, Customer Service, and Order Service β communicating asynchronously using Spring WebFlux and WebClient.
Product Service β Manages product details and stock.
Handles CRUD operations for products.
Exposes endpoints for GET, POST, PUT, PATCH, and DELETE.
The PATCH endpoint updates stock after order placement.
Customer Service β Manages customer information.
Handles customer data CRUD operations.
Independent service used by Order Service to validate customers.
Order Service β Places orders and communicates with both Product and Customer services.
The core service demonstrating async communication.
Uses WebClient to call Product and Customer services.
Includes examples of chaining, composition, and parallel calls.
In Order Service :
Defining as a Spring Bean for reuse.
@Configuration
public class WebClientConfig {
@Bean
public WebClient productWebClient() {
HttpClient httpClient = HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(5))
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS))
.addHandlerLast(new WriteTimeoutHandler(5, TimeUnit.SECONDS)));
return WebClient.builder().baseUrl("http://localhost:8081/products")
.clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}
@Bean
public WebClient customerWebClient() {
HttpClient httpClient = HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(5))
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS))
.addHandlerLast(new WriteTimeoutHandler(5, TimeUnit.SECONDS)));
return WebClient.builder().baseUrl("http://localhost:8082/customer")
.clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}
}
Creating DTO Classes.
ProductResponse
@Data
public class ProductResponse {
private Long productId;
private String productName;
private String productCategory;
private Double productPrice;
private Integer stock;
}
CustomerResponse
@Data
public class CustomerResponse {
private Long customerId;
private String customerName;
private String email;
private String address;
}
Creating Service.
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final WebClient customerClient;
private final WebClient productClient;
@Autowired
public OrderService(OrderRepository orderRepository, WebClient customerWebClient, WebClient productWebClient) {
this.orderRepository = orderRepository;
this.customerClient = customerWebClient;
this.productClient = productWebClient;
}
// Get Customer by ID
public Mono<CustomerResponse> getCustomerById(Long customerId) {
return customerClient.get()
.uri("/{id}", customerId)
.retrieve()
.onStatus(status -> status.is4xxClientError(), clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Customer not found")))
)
.onStatus(status -> status.is5xxServerError(), clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Customer service error")))
)
.bodyToMono(CustomerResponse.class)
.timeout(Duration.ofSeconds(5))
.doOnNext(c -> System.out.println("Fetched customer: " + c))
.doOnError(e -> System.out.println("Error fetching customer: " + e.getMessage()));
}
// Get Product by ID
public Mono<ProductResponse> getProductById(Long productId) {
return productClient.get()
.uri("/{id}", productId)
.retrieve()
.onStatus(status -> status.is4xxClientError(), clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Product not found")))
)
.onStatus(status -> status.is5xxServerError(), clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Product service error")))
)
.bodyToMono(ProductResponse.class)
.timeout(Duration.ofSeconds(5))
.onErrorResume(e -> {
System.out.println("Fallback triggered for product: " + e.getMessage());
ProductResponse fallback = new ProductResponse();
fallback.setProductId(productId);
fallback.setProductName("Fallback Product");
fallback.setStock(0);
fallback.setProductPrice(0.0);
return Mono.just(fallback);
});
}
// Reduce stock
public Mono<ProductResponse> reduceStock(Long productId, int stock) {
return productClient.patch()
.uri(uriBuilder -> uriBuilder
.path("/{id}/reduceStock")
.queryParam("stock", stock)
.build(productId))
.retrieve()
.onStatus(status -> status.is4xxClientError(), clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Client error: " + body)))
)
.onStatus(status -> status.is5xxServerError(), clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Server error: " + body)))
)
.bodyToMono(ProductResponse.class)
.timeout(Duration.ofSeconds(5))
.doOnNext(p -> System.out.println("Stock reduced: " + p))
.doOnError(e -> System.out.println("Error calling reduceStock: " + e.getMessage()));
}
// Place Order
public Mono<Order> placeOrder(Order order) {
return getCustomerById(order.getCustomerId())
.flatMap(customer ->
getProductById(order.getProductId())
.flatMap(product -> {
if(product.getStock() < order.getQuantity()) {
return Mono.error(new RuntimeException("Insuffient Stock...!!!"));
}
return reduceStock(order.getProductId(), order.getQuantity())
.flatMap(updatedProduct -> {
order.setTotalPrice(order.getQuantity() * product.getProductPrice());
order.setCreatedAt(LocalDateTime.now());
return orderRepository.save(order) ;
});
})
)
.doOnError(e -> System.out.println("Error in placing order: " + e.getMessage()));
}
}
Create Controller.
@RestController
@RequestMapping("/order")
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/create")
public Mono<Order> placeOrder(@RequestBody Order order) {
// Return the reactive stream directly
return orderService.placeOrder(order);
}
}
Curl:
curl --location 'http://localhost:8083/order/create' \
--header 'Content-Type: application/json' \
--data '{
"customerId": 2,
"productId": 2,
"quantity": 1
}'
Conclusion
WebClient in Spring Boot offers a powerful and reactive way to handle inter-service communication. Through this blog, we explored configuration, asynchronous calls, chaining, composition, and error handling with Mono and Flux. The example project demonstrated how multiple services can interact efficiently in a non-blocking manner, making WebClient an ideal choice for modern microservice communication.
Top comments (0)