Enterprise Java

Spring WebClient Synchronous Requests Example

Spring Framework has evolved to provide a wide variety of ways to interact with external systems, especially through HTTP requests. One of the most popular libraries for making these HTTP calls is the WebClient, which is part of the Spring WebFlux module. While WebClient is commonly associated with asynchronous non-blocking calls, it is also capable of making synchronous requests, depending on the use case. Let us delve into understanding Java WebClient synchronous requests.

1. Overview of HTTP Client Libraries in Spring

Spring Framework provides several HTTP client libraries to interact with external services:

  • RestTemplate: A synchronous, blocking HTTP client that has been the standard way of making HTTP calls in Spring applications for many years. RestTemplate provides a simple and intuitive API for interacting with RESTful services, supporting common HTTP methods such as GET, POST, PUT, and DELETE. However, RestTemplate is now in maintenance mode, meaning that while it will still receive bug fixes, no new features or enhancements will be added. One of the reasons for this shift is that RestTemplate is inherently blocking, which is not ideal for modern, high-performance, reactive systems where non-blocking I/O can offer better scalability and responsiveness. As a result, WebClient has been introduced as its modern, non-blocking alternative.
  • WebClient: Introduced as part of the Spring WebFlux module, WebClient is designed for both synchronous and asynchronous calls. It is a highly flexible, non-blocking HTTP client that can reactively interact with external services. This makes it a suitable choice for modern web applications that need to handle a large number of concurrent requests with minimal thread usage. While WebClient shines in reactive programming by using Mono and Flux (reactive data types) to handle asynchronous operations, it can also be used synchronously by simply using the block() method, allowing developers to choose the right strategy depending on their application’s architecture. Key features of WebClient include:
    • Support for both synchronous and asynchronous calls.
    • Streaming large data sets with backpressure handling using Flux.
    • Customizable request and response handling (e.g., adding headers, custom error handling).
    • Integration with other reactive libraries, such as Reactor and RxJava.
  • OkHttpClient: A third-party HTTP client widely adopted in the Java community for both blocking and non-blocking calls. Developed by Square, OkHttp is known for being fast, efficient, and easy to integrate. It is designed to be simple yet capable of handling complex scenarios such as connection pooling, transparent GZIP compression, and caching of responses to improve performance. OkHttp can be used in Spring applications either on its own or through libraries like Retrofit (another HTTP client built on top of OkHttp). Additionally, it can be integrated into Spring Boot projects, where it is often used as a replacement for RestTemplate in blocking mode or paired with WebClient for non-blocking use cases. OkHttp is known for:
    • Fast and efficient HTTP communication, with good resource management.
    • Support for HTTP/2 and WebSocket connections.
    • Transparent handling of retries, redirects, and connection pooling.
    • Easy integration with third-party tools like Retrofit or Spring applications.

Each of these libraries has its strengths and use cases, and choosing the right one depends on your application’s requirements. While RestTemplate is simple and reliable for blocking calls, WebClient offers the flexibility of both synchronous and asynchronous approaches and is the recommended tool for modern Spring applications. OkHttpClient is an excellent alternative for those looking for a highly performant third-party solution.

2. Blocking vs. Non-blocking API Calls

A key difference between blocking and non-blocking requests lies in how they handle I/O operations:

  • Blocking (Synchronous): The thread that initiates the request waits until the response is received, blocking further execution.
  • Non-blocking (Asynchronous): The thread does not wait for the response. Instead, the request executes in the background, allowing other tasks to proceed without blocking.
  • Resource Utilization:
    • Blocking: Uses a thread per request, which can be resource-intensive, especially under high load. A large number of blocking threads can lead to performance bottlenecks.
    • Non-blocking: Uses fewer threads, as requests are handled asynchronously. This leads to better resource utilization, especially in highly concurrent environments.
  • Scalability:
    • Blocking: Limited scalability. As the number of requests increases, the number of threads required grows, leading to higher memory and CPU usage.
    • Non-blocking: Highly scalable. Since non-blocking I/O uses fewer threads, the system can handle a higher number of concurrent requests efficiently.
  • Performance:
    • Blocking: Slower in high-concurrency environments due to thread contention. Each thread must wait for its task to complete, leading to increased latency.
    • Non-blocking: Faster in high-concurrency environments, as the system can handle more requests simultaneously without threads being tied up waiting for I/O operations.
  • Error Handling:
    • Blocking: Errors are handled straightforwardly since the calling thread either gets the response or catches an exception synchronously.
    • Non-blocking: Requires more sophisticated error handling, often using callbacks, reactive streams, or promises to deal with potential failures asynchronously.
  • Complexity:
    • Blocking: Simpler to implement, as the control flow is linear and easy to understand.
    • Non-blocking: More complex to implement and reason about, as it involves dealing with callbacks, futures, or reactive programming paradigms (e.g., Mono/Flux in Spring).
  • Use Case:
    • Blocking: Suitable for smaller applications with limited concurrency or when thread management is not a concern.
    • Non-blocking: Ideal for high-performance applications like real-time systems, microservices, or reactive systems where scalability and low latency are critical.

3. When to Use Synchronous Requests

Synchronous requests are appropriate when:

  • Data consistency or order is important (e.g., financial transactions): In scenarios where the exact order of operations is crucial, such as in banking or e-commerce systems, synchronous requests ensure that operations are performed sequentially. For example, in a financial transaction, debit and credit operations must occur in a specific order, and a synchronous approach guarantees this strict order by waiting for one operation to complete before initiating the next.
  • You are working in a traditional monolithic or blocking environment: Many legacy or monolithic systems are built around blocking paradigms where synchronous calls are the norm. In such environments, using synchronous requests is simpler to implement and maintain because they follow a linear execution flow. Refactoring these systems to non-blocking approaches may require substantial changes, including adapting existing libraries and frameworks.
  • Resources are limited and can be easily managed with a few long-running threads: For applications with low concurrency and predictable workloads, synchronous calls work well. In these scenarios, the overhead of managing many threads is low, and it’s easier to monitor and control system performance. A small set of long-running threads that handle synchronous requests can efficiently manage the application’s workload without introducing the complexity of asynchronous thread management.
  • Debugging and troubleshooting are easier: Since synchronous requests follow a straightforward, sequential flow, debugging is typically easier than with asynchronous or non-blocking requests. This makes it ideal for applications where developers need to quickly diagnose and fix issues by following the program’s linear execution path.
  • Response time is not critical: In systems where real-time performance and fast response times are not critical, the blocking nature of synchronous requests is acceptable. These systems may involve backend batch processing or administrative tools where the time taken for a request to complete is not as sensitive.

However, in modern reactive systems, non-blocking requests are generally more efficient. These systems are designed to handle a high number of concurrent requests without tying up resources, which makes them ideal for web-scale applications, real-time communication, and microservices architectures. Non-blocking requests allow the system to process other tasks while waiting for I/O operations to complete, making better use of available CPU resources and improving scalability under high loads.

4. Synchronous API Calls With WebClient

Although WebClient is typically used for non-blocking I/O operations, it can be configured to make synchronous calls by blocking the current thread using the block() method.

4.1 Practical Example

Let’s create a practical example where WebClient is used to make a synchronous POST request to a fictional API that accepts JSON data and returns a confirmation message.

import org.springframework.web.reactive.function.client.WebClient;

public class WebClientPostSyncExample {
    public static void main(String[] args) {
        // Create a WebClient instance with the base URL
        WebClient webClient = WebClient.create("https://api.example.com");

        // JSON request body
        String requestBody = "{ \"name\": \"John Doe\", \"email\": \"john@example.com\" }";

        // Make a synchronous POST request
        String response = webClient.post()
                                   .uri("/users")
                                   .header("Content-Type", "application/json")
                                   .bodyValue(requestBody)
                                   .retrieve()
                                   .bodyToMono(String.class)
                                   .block();  // This makes the call synchronous

        // Print the response
        System.out.println(response);
    }
}

4.1.1 Code Breakdown

  • post(): Specifies the HTTP method as POST.
  • header(“Content-Type”, “application/json”): Adds a header to the request specifying the content type as JSON.
  • bodyValue(requestBody): Sets the body of the POST request to the JSON string requestBody.
  • retrieve(): Sends the request and prepares to retrieve the response.
  • block(): The synchronous call waits for the response to be received before continuing execution.

4.1.2 Code Output

The out of the code will depend on response returned by the api. If everything goes well the following output will be returned.

{"id":123,"name":"John Doe","email":"john@example.com"}

If api returns an error the org.springframework.web.reactive.function.client.WebClientRequestException: ... will be thrown.

5. Conclusion

While WebClient is a versatile tool primarily designed for non-blocking, asynchronous operations, it can be used for synchronous requests when necessary. By calling the block() method, WebClient forces the calling thread to wait for the response, mimicking the behavior of traditional blocking libraries like RestTemplate. However, use synchronous calls wisely, as they can lead to performance bottlenecks in reactive systems designed for concurrency. When scalability and performance are key, prefer non-blocking, reactive programming models.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button