Hello! Welcome to the next lesson in our module on Inter-Service Communication Patterns.
In our last session, we mastered OpenFeign for creating clean, declarative synchronous clients. We saw how it simplifies code and how we can add resilience through timeouts and retries. However, synchronous communication has an inherent limitation: the calling thread is blocked, waiting for a response. In high-traffic systems, this can lead to thread exhaustion and limit scalability.
Today, we shift our focus to the asynchronous, non-blocking paradigm, which is a cornerstone of modern, high-performance microservices. By the end of this lesson, you will be able to implement asynchronous, non-blocking communication using WebClient. This skill is crucial for building highly concurrent applications and is a frequent topic in senior-level technical interviews.
1. The Landscape of Spring HTTP Clients
Before we dive in, let's contextualize WebClient. Spring provides several tools for making HTTP calls, each suited for different use cases.

RestTemplate is the legacy, blocking client. OpenFeign, as we learned, provides a declarative wrapper (often around a blocking client). WebClient is fundamentally different because it is asynchronous and non-blocking from the ground up.
2. The "Why": Understanding the Non-Blocking Model
To appreciate why WebClient is so powerful, we need to understand the architectural shift it represents.
Traditional synchronous clients, used in a standard Spring MVC application, run on a thread-per-request model. A server like Tomcat maintains a pool of worker threads. When a request comes in, a thread is assigned to handle it from start to finish. If that handler makes a remote API call, the thread blocks—it sits idle, consuming memory, waiting for the remote service to respond. For a system with many concurrent I/O-bound operations (like calling other microservices), this is inefficient and doesn't scale well.
Spring WebFlux, the framework that provides WebClient, uses a different approach powered by servers like Netty. It employs an Event Loop model.
Spring Boot WebFlux Complete Flow | Netty Event Loop Explained ! | Reactive Programming
To understand this fundamental concept, let's watch a segment from the video 'Spring Boot WebFlux Complete Flow' from the Selenium Express channel. It provides an excellent explanation of how the Event Loop works.
Watch from 01:55 to 12:50. Focus on these key ideas: What the Event Loop is (a single thread that can handle many requests). How it uses a task queue to process events. How it avoids blocking by offloading I/O-intensive operations and continuing to serve other requests.
In essence, an Event Loop is a thread that continuously checks a queue for new events (like an incoming request or a completed I/O operation). When it picks up a task that involves I/O (like an HTTP call), it initiates the operation and, instead of waiting, registers a callback. It then moves on to the next event in the queue. When the I/O operation completes, a new "response received" event is added to the queue, which the loop will eventually process.
This model allows a small number of threads to handle a massive number of concurrent connections, making it ideal for I/O-heavy microservices.
3. Introducing WebClient, Mono, and Flux
WebClient is the HTTP client designed for this reactive, event-driven world. Instead of returning a direct object and blocking the thread, it returns a publisher. The two main publishers you'll work with are from Project Reactor:
Mono<T>: A publisher that emits 0 or 1 item. Used for requests that return a single resource or no result (e.g., fetching an entity by ID, creating a resource).Flux<T>: A publisher that emits 0 to N items. Used for requests that return a collection or a stream of data (e.g., fetching a list of all entities).
These reactive types are like a promise or a blueprint for a future value. The actual HTTP call is not made until you subscribe to the publisher.
Let's see how this changes the way we write code.
🚀 Mastering Asynchronous API Calls with Spring WebClient
The article 'Mastering Asynchronous API Calls with Spring WebClient' provides a clear distinction between blocking and non-blocking calls.
Read the section 'Blocking vs. Non-blocking API Calls'. Pay close attention to the code examples. Notice how the non-blocking version returns a Mono, while the blocking version calls .block() to wait for the result.
- Non-blocking (the right way): Your controller returns a
Mono<ProductResponse>. The framework handles subscribing and writing the response when it arrives, without blocking the request-handling thread. - Blocking (the anti-pattern): Calling
.block()on aMonoorFluxdefeats the purpose of reactive programming. It forces the thread to wait, effectively turning your non-blocking call into a blocking one. This should be avoided in a reactive pipeline and is mostly used in tests or when integrating with legacy blocking code.
4. Implementing WebClient in Spring Boot
Let's get practical. The following video provides a great walkthrough of setting up and using WebClient.
Getting Started with the Web Client in Spring Boot & Writing Tests
In 'Getting Started with the Web Client in Spring Boot & Writing Tests' by Dan Vega, we'll see a step-by-step implementation of a client.
Watch from the beginning to 08:00. This segment covers: Setting up the spring-boot-starter-webflux dependency. Creating a client component that uses WebClient. Making a GET request and understanding the need to subscribe() to trigger the execution.
Here's a summary of the best practices for implementation:
Step 1: Add the WebFlux Starter Dependency
In your pom.xml, include the spring-boot-starter-webflux dependency. This brings in WebClient and the underlying Project Reactor and Netty libraries.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Step 2: Configure a WebClient Bean
It's a best practice to create a WebClient instance as a Spring bean, allowing for centralized configuration and reuse. You can configure base URLs, default headers, timeouts, and more.
@Configuration
public class WebClientConfig {
@Bean
public WebClient paymentServiceClient() {
// Example with connection timeouts
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 5 seconds
.responseTimeout(Duration.ofSeconds(5));
return WebClient.builder()
.baseUrl("http://localhost:8082/api/payments")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}
Tip: For production, you'd externalize the base URL to your application.yml.
Step 3: Implement Service Calls
Now, inject your WebClient bean and use its fluent API to build requests.
The article 'Mastering Spring WebClient' by Isuru Pradeep offers clear examples for all standard HTTP methods.
Review the code snippets in the section '3. Making HTTP Requests with WebClient'. Notice the patterns for GET (Mono vs. Flux), POST, PUT, and DELETE requests.
Here is a consolidated example:
@Service
public class PaymentService {
private final WebClient paymentServiceClient;
@Autowired
public PaymentService(WebClient paymentServiceClient) {
this.paymentServiceClient = paymentServiceClient;
}
// GET a single resource
public Mono<PaymentDto> getPaymentById(String id) {
return paymentServiceClient.get()
.uri("/{id}", id)
.retrieve()
.bodyToMono(PaymentDto.class);
}
// GET a collection of resources
public Flux<PaymentDto> getAllPayments() {
return paymentServiceClient.get()
.uri("/all")
.retrieve()
.bodyToFlux(PaymentDto.class);
}
// POST to create a resource
public Mono<PaymentConfirmation> createPayment(PaymentRequest request) {
return paymentServiceClient.post()
.uri("/process")
.bodyValue(request)
.retrieve()
.bodyToMono(PaymentConfirmation.class);
}
// DELETE a resource
public Mono<Void> cancelPayment(String id) {
return paymentServiceClient.delete()
.uri("/{id}", id)
.retrieve()
.bodyToMono(Void.class); // Use Void.class for empty responses
}
}
5. Composing Asynchronous Operations
The real power of reactive programming emerges when you start composing operations. In a microservices architecture, you often need to orchestrate multiple calls.
🚀 Mastering Asynchronous API Calls with Spring WebClient
Let's return to 'Mastering Asynchronous API Calls with Spring WebClient' for a look at powerful composition patterns.
Read the sections 'Chaining, Composition, and Parallel Calls' and 'Exception Handling'. Focus on the use of flatMap for sequential calls, zip for parallel calls, and onStatus for reactive error handling.
Sequential Dependent Calls: flatMap
Use flatMap when the next call depends on the result of the previous one. For example, to place an order, you might first need to fetch user details.
public Mono<OrderConfirmation> placeOrder(OrderRequest orderRequest) {
// 1. Fetch the customer details first
return customerClient.get()
.uri("/customers/{id}", orderRequest.getCustomerId())
.retrieve()
.bodyToMono(CustomerDto.class)
// 2. Once customer is fetched, use flatMap to call the product service
.flatMap(customer ->
productClient.get()
.uri("/products/{id}", orderRequest.getProductId())
.retrieve()
.bodyToMono(ProductDto.class)
)
// 3. More processing can be chained here...
.map(product -> /* ... create and return confirmation ... */);
}
Parallel Independent Calls: zip
Use Mono.zip when you need to make multiple independent calls and combine their results. This is much more efficient than making them sequentially.
public Mono<EnrichedOrderDetails> getOrderDetails(String orderId) {
// Assume getOrder(orderId) returns a Mono<Order>
Mono<Order> orderMono = getOrder(orderId);
// After getting the order, we need customer and product details.
// These two calls are independent of each other.
return orderMono.flatMap(order -> {
Mono<CustomerDto> customerMono = customerClient.get().uri("/customers/{id}", order.getCustomerId()).retrieve().bodyToMono(CustomerDto.class);
Mono<ProductDto> productMono = productClient.get().uri("/products/{id}", order.getProductId()).retrieve().bodyToMono(ProductDto.class);
// Mono.zip executes both monos in parallel and combines results into a Tuple
return Mono.zip(customerMono, productMono)
.map(tuple -> new EnrichedOrderDetails(order, tuple.getT1(), tuple.getT2()));
});
}
Test your understanding!
You are building a dashboard for an e-commerce platform. To display the main page, you need to:
- Fetch the logged-in user's profile from the
user-service. - Once you have the user's profile, use their ID to fetch their recent order history from the
order-service. - Independently, you also need to fetch a list of featured products from the
product-service.
Which reactive operators (flatMap, zip) would you use to orchestrate these calls efficiently? Describe the sequence.
Show answer
Here's the efficient orchestration strategy:
- Start by fetching the user profile (
userMono). - Use
flatMapon theuserMono. The logic inside theflatMapwill only execute after the user profile has been successfully fetched. - Inside the
flatMap, you now have two independent tasks: fetching the user's orders (which requires theuserId) and fetching the featured products (which requires no input from the user). - Create a
Monofor fetching orders (ordersMono) and anotherMonofor fetching featured products (productsMono). - Use
Mono.zip(ordersMono, productsMono)to execute both of these calls in parallel. - The result of the
zipwill be aTuplecontaining both the order history and the list of featured products, which you can then map to a final dashboard DTO.
This approach correctly models the dependency (orders depend on user) while maximizing parallelism for the independent operations (orders vs. products).
Conclusion
You've now learned the fundamentals of asynchronous communication with WebClient. This approach is key to building microservices that are scalable, resilient, and efficient with their resources.
Key Takeaways:
- Paradigm Shift:
WebClientoperates on an event loop model, allowing a few threads to handle many concurrent I/O-bound requests without blocking. - Reactive Types: Communication is built around
Mono(for 0-1 items) andFlux(for 0-N items), which are non-blocking publishers. - Implementation: Use the
spring-boot-starter-webfluxdependency and configureWebClientas a bean for reuse and centralized management. - Composition is Key: Use
flatMapfor sequential, dependent calls andzipfor parallel, independent calls to orchestrate complex workflows efficiently.
Next Up
So far, we have been calling other services using hardcoded URLs like http://localhost:8081. In a real, dynamic microservices environment, service instances come and go, and their locations (IP addresses and ports) can change. How does a client service find the correct, healthy instance of a server service? In our next lesson, we will solve this problem by exploring service discovery. You will explain the role of a service registry and compare client-side vs. server-side discovery patterns.
Can't find a good explanation? Sign up and we'll make it for you
Sign up