In the era of cloud-native applications and microservices, synchronous, blocking HTTP clients like RestTemplate fall short in terms of performance and scalability. To address this limitation, Spring WebFlux offers WebClient — a powerful, reactive HTTP client that supports non-blocking, asynchronous communication.

In this article, we’ll explore how WebClient works within the Spring WebFlux framework, its advantages over traditional clients, and how to use it to communicate between microservices or with external APIs. We’ll provide code examples for real-world scenarios and explain how to handle errors, retries, timeouts, and streaming responses.

Understanding WebClient and Its Role in Spring WebFlux

Spring WebFlux is a reactive web framework built on Project Reactor, part of the Spring 5 ecosystem. Unlike Spring MVC (which is servlet-based and blocking), WebFlux is non-blocking, asynchronous, and event-driven.

WebClient is the successor of RestTemplate and the preferred way to make HTTP requests in Spring WebFlux:

  • It uses reactive types like Mono and Flux.

  • Supports asynchronous backpressure-aware communication.

  • Works well in high-concurrency environments due to its non-blocking nature.

Adding WebClient to Your Spring Boot Application

To use WebClient, make sure you include the Spring WebFlux dependency in your pom.xml or build.gradle.

For Maven:

xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

For Gradle:

groovy
implementation 'org.springframework.boot:spring-boot-starter-webflux'

Creating a WebClient Bean

A common practice is to create a reusable, centralized WebClient bean configured with a base URL, default headers, and other customizations.

java
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder
.baseUrl(“http://localhost:8081”)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}

Or use the builder directly:

java
WebClient webClient = WebClient.builder()
.baseUrl("http://external-service.com")
.build();

Making GET Requests with WebClient

Suppose we need to fetch user details from another microservice:

java
@Service
public class UserService {
private final WebClient webClient;public UserService(WebClient webClient) {
this.webClient = webClient;
}public Mono<User> getUserById(String userId) {
return webClient.get()
.uri(“/users/{id}”, userId)
.retrieve()
.bodyToMono(User.class);
}
}

This returns a Mono<User> that can be subscribed to or returned directly from a controller endpoint.

Making POST Requests

To send data using a POST request:

java
public Mono<User> createUser(User user) {
return webClient.post()
.uri("/users")
.bodyValue(user)
.retrieve()
.bodyToMono(User.class);
}

bodyValue or body(BodyInserter) is used to serialize the request payload.

Handling Errors Gracefully

Use the onStatus method for HTTP error handling:

java
public Mono<User> getUserSafely(String userId) {
return webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response ->
Mono.error(new RuntimeException("User not found")))
.onStatus(HttpStatus::is5xxServerError, response ->
Mono.error(new RuntimeException("Server error")))
.bodyToMono(User.class);
}

This makes the client more resilient by allowing you to map specific HTTP errors to application-level exceptions.

Timeout and Retry Mechanism

Handling transient failures with retries and timeouts is crucial in microservice communication.

java
public Mono<User> getUserWithTimeoutAndRetry(String userId) {
return webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(User.class)
.timeout(Duration.ofSeconds(3))
.retryWhen(Retry.backoff(3, Duration.ofMillis(500)));
}

This ensures the client retries failed calls and fails fast on slow responses.

Streaming Responses with Flux

WebClient can handle Server-Sent Events (SSE) or any streaming responses using Flux.

java
public Flux<Notification> streamNotifications() {
return webClient.get()
.uri("/notifications/stream")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Notification.class);
}

This is particularly useful for real-time dashboards or alert systems.

Using WebClient in Controller Layer

You can directly return reactive types in Spring WebFlux controller methods:

java
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;public UserController(UserService userService) {
this.userService = userService;
}@GetMapping(“/users/{id}”)
public Mono<User> getUser(@PathVariable String id) {
return userService.getUserById(id);
}@PostMapping(“/users”)
public Mono<User> createUser(@RequestBody User user) {
return userService.createUser(user);
}
}

This allows fully reactive request handling, with WebClient managing outbound calls reactively.

Customizing WebClient with Filters

Filters are like interceptors and can be used to log, modify requests, or handle authorization.

java
@Bean
public WebClient webClientWithFilters(WebClient.Builder builder) {
return builder
.baseUrl("http://localhost:8081")
.filter((request, next) -> {
System.out.println("Request: " + request.url());
return next.exchange(request);
})
.build();
}

You can also use filters for OAuth2 token management, retries, and correlation IDs in distributed tracing.

Secure WebClient with OAuth2

If your microservice needs to call a secured API:

java
@Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager manager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager);
oauth2.setDefaultClientRegistrationId("my-client");
return WebClient.builder()
.apply(oauth2.oauth2Configuration())
.build();
}

This configuration ensures tokens are injected automatically in outgoing requests.

Unit Testing WebClient-Enabled Services

When unit testing, use WebClient.Builder with ExchangeFunction to simulate server responses.

java
@Test
void testGetUser() {
ExchangeFunction exchangeFunction = request -> {
ClientResponse response = ClientResponse
.create(HttpStatus.OK)
.header("Content-Type", "application/json")
.body("{\"id\":\"123\",\"name\":\"Alice\"}")
.build();
return Mono.just(response);
};WebClient webClient = WebClient.builder().exchangeFunction(exchangeFunction).build();
UserService userService = new UserService(webClient);StepVerifier.create(userService.getUserById(“123”))
.expectNextMatches(user -> user.getName().equals(“Alice”))
.verifyComplete();
}

This avoids real network calls during tests.

Pros and Use Cases of WebClient

Why use WebClient?

  • Enables non-blocking I/O, making it scalable under high loads.

  • Seamless integration with Project Reactor and Spring WebFlux.

  • Supports streaming, retries, filters, and advanced customizations.

  • Ideal for reactive microservices and event-driven systems.

Best Use Cases:

  • Calling internal microservices from reactive services.

  • Streaming API data in real-time apps (dashboards, alerts).

  • Connecting to external APIs without blocking threads.

  • Consuming large volumes of concurrent requests.

Conclusion

Spring WebFlux’s WebClient represents a significant leap forward in building resilient, scalable, and non-blocking communication pipelines in reactive microservices. Unlike its predecessor RestTemplate, WebClient fully embraces the reactive programming paradigm, using Mono and Flux to model asynchronous sequences, reduce thread contention, and improve overall throughput.

By decoupling HTTP communication from blocking I/O operations, developers gain powerful tools to:

  • Compose async pipelines.

  • Handle transient failures with elegant retry mechanisms.

  • Process continuous data streams from SSE or WebSockets.

  • Secure communication with advanced OAuth2 integrations.

  • Write non-blocking integration tests for reactive endpoints.

For modern systems designed to thrive under concurrency, elasticity, and speed, adopting WebClient in place of traditional blocking clients is not just a recommendation—it’s a necessity. It aligns perfectly with the goals of reactive systems: resilience, responsiveness, scalability, and elasticity.

Whether you’re building microservices that talk to each other or consuming third-party APIs, WebClient in Spring WebFlux enables you to unlock the full potential of reactive programming in a robust, idiomatic, and highly performant way.