Create your own
Lesson illustration

Asynchronous Communication with WebClient

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.

Comparison of Spring HTTP Clients: RestTemplate, WebClient, and RestClient
This table compares RestTemplate, WebClient, and the newer RestClient. As you can see, WebClient is designed for the reactive stack, making it the primary choice for asynchronous, non-blocking communication.

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 a Mono or Flux defeats 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.

Mastering Spring WebClient

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:

  1. Fetch the logged-in user's profile from the user-service.
  2. Once you have the user's profile, use their ID to fetch their recent order history from the order-service.
  3. 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:

  1. Start by fetching the user profile (userMono).
  2. Use flatMap on the userMono. The logic inside the flatMap will only execute after the user profile has been successfully fetched.
  3. Inside the flatMap, you now have two independent tasks: fetching the user's orders (which requires the userId) and fetching the featured products (which requires no input from the user).
  4. Create a Mono for fetching orders (ordersMono) and another Mono for fetching featured products (productsMono).
  5. Use Mono.zip(ordersMono, productsMono) to execute both of these calls in parallel.
  6. The result of the zip will be a Tuple containing 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: WebClient operates 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) and Flux (for 0-N items), which are non-blocking publishers.
  • Implementation: Use the spring-boot-starter-webflux dependency and configure WebClient as a bean for reuse and centralized management.
  • Composition is Key: Use flatMap for sequential, dependent calls and zip for 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