Inter-Service Communication with WebClient
Inter-Service Communication with WebClient
Once you have two microservices running, they inevitably need to talk to each other over HTTP. Spring Boot 3 ships with two first-party HTTP clients: the older blocking RestTemplate (now in maintenance mode) and the modern, non-blocking WebClient from Spring WebFlux. Even if your service is built on the traditional servlet stack rather than a reactive one, WebClient is the recommended choice — it can be used in a synchronous blocking style while still being far more configurable and testable than RestTemplate.
Why WebClient Instead of RestTemplate?
RestTemplate was introduced in Spring 3.0 and is synchronous by nature: each call blocks the calling thread until the remote service responds. The Spring team declared it in maintenance mode as of Spring 5.0. WebClient, by contrast, was built from scratch for a non-blocking world but also supports blocking calls, making it a drop-in replacement with a better feature set:
- Streaming support — can receive
text/event-streamor large payloads without buffering the whole body. - Rich filter pipeline — interceptors (called ExchangeFilterFunction) let you attach logging, auth headers, retries, and circuit breakers in one place.
- Reactive or blocking — call
.block()when you need a synchronous result; leave it reactive when you want non-blocking I/O. - Built-in codec support — automatic JSON serialization and deserialization with Jackson; no extra boilerplate.
spring-boot-starter-webflux to use WebClient. Add spring-boot-starter-web (servlet) plus spring-webflux as a direct dependency and you get WebClient without switching your whole application to reactive programming.
Adding the Dependency
If your service already uses spring-boot-starter-webflux, WebClient is already on the classpath. If you are on the servlet stack, add only the reactive web module:
Creating a WebClient Bean
Always create WebClient instances through a WebClient.Builder bean that Spring Boot auto-configures. This builder carries any globally registered ExchangeFilterFunctions — including those added by Spring Cloud for distributed tracing — so never instantiate WebClient.create() manually in production code.
Declare one bean per downstream service. Using a dedicated, named bean rather than a shared one keeps base URLs, default headers, and filters scoped to the correct service.
Making a GET Request
The following shows how the order-service calls the inventory-service to check stock for a given product ID.
Breaking down the fluent chain:
.get()— starts a GET request spec..uri(...)— appends the path; URI template variables are expanded safely (no string concatenation, no injection risk)..retrieve()— triggers the request and gives access to the response body. Automatically throwsWebClientResponseExceptionfor 4xx/5xx status codes..bodyToMono(StockResponse.class)— deserializes the JSON body into your DTO using Jackson..block()— blocks the calling thread until the response arrives. Acceptable in a servlet-stack service; avoid in a reactive service.
Making a POST Request with a Body
.bodyValue() for a single object and .body(BodyInserters.fromValue(...)) when you need more control (e.g., multipart forms). Both serialize with the same Jackson ObjectMapper configured globally in your application.
Handling HTTP Errors Explicitly
By default .retrieve() maps 4xx responses to WebClientResponseException.BadRequest and 5xx to WebClientResponseException.InternalServerError. You can intercept specific status codes and translate them into domain exceptions:
Adding Authentication Headers
In a real microservices deployment, service-to-service calls often carry a JWT or an internal API key. Apply it via an ExchangeFilterFunction so every request from this client automatically includes it — no per-call boilerplate:
Timeouts — An Essential Safety Net
Without timeouts, a slow downstream service will hold your thread (or subscription) indefinitely, eventually exhausting connection pools and cascading into a system-wide outage. Always set both a connection timeout and a read timeout:
A common production rule of thumb: set the connection timeout short (1–3 s) and the response timeout no more than the SLA you want to expose to your own callers. If the inventory service promises 3-second p99 latency, your timeout should be 4–5 seconds to give it a margin but still fail fast.
Summary
WebClient is the standard tool for HTTP-based inter-service communication in Spring Boot 3. Obtain it via the auto-configured WebClient.Builder, scope one bean per downstream service, use URI templates to avoid injection risks, translate HTTP error codes into domain exceptions with .onStatus(), attach auth tokens through ExchangeFilterFunction, and always configure explicit connection and response timeouts. In the next lesson you will see how OpenFeign lets you express the same calls as a declarative interface with even less boilerplate.