In a Spring MVC (servlet) application, when would you choose RestClient over WebClient, and what are the concurrency/thread implications?
answer
- RestClient sync, WebClient reactive — same fluent API
- servlet = thread-per-request; blocking call holds a thread
- WebClient.block() = worst of both worlds
- WebClient for fan-out/streaming/already-reactive
- virtual threads (Loom) make blocking cheap again
basics
~10 sIn a blocking servlet app, prefer RestClient: it's synchronous, needs no reactive stack, and matches the thread-per-request model. Use WebClient only when you genuinely need non-blocking/async/streaming or high fan-out concurrency.
solid answer
~50 sRestClient and WebClient share a fluent API, but RestClient is synchronous and WebClient is reactive. In a Spring MVC servlet app, each request runs on a dedicated thread; a blocking outbound call with RestClient fits that thread-per-request model cleanly, adds no reactor dependency, and keeps stack traces and debugging simple. WebClient's advantage is non-blocking I/O — you can fire many concurrent outbound calls without tying up one thread each, and it supports streaming/backpressure. But calling WebClient.block() inside a servlet just reintroduces blocking with extra overhead and pitfalls. So: default to RestClient for straightforward blocking calls; reach for WebClient when you need real async parallelism (large fan-out), streaming responses, or you're already on WebFlux. The concurrency cost of RestClient is that each in-flight call holds a servlet thread, so timeouts and a bounded connection pool are essential to avoid thread exhaustion.
code
java · 17 lines// Servlet (MVC) app: blocking call — RestClient is the natural fit
@Service
class PricingClient {
private final RestClient rest;
PricingClient(RestClient.Builder builder) { // inherits Boot's factory/timeouts
this.rest = builder.baseUrl("https://pricing.internal").build();
}
Price fetch(String sku) {
return rest.get().uri("/price/{sku}", sku)
.retrieve()
.body(Price.class); // blocks this servlet thread until done
}
}
// Anti-pattern in a servlet app: WebClient just to call .block()
// Price p = webClient.get().uri(...).retrieve().bodyToMono(Price.class).block();
// -> still blocks the thread AND adds reactor overhead; prefer RestClient.go deeper
Know RestClient is for blocking calls and WebClient is reactive; in MVC use RestClient.
Explain thread-per-request and why a blocking client fits it; recognize WebClient.block() as a smell.
Weigh fan-out/streaming needs, discuss thread-pool exhaustion and the mitigations (timeouts, pooling, resilience).
Set the org's client strategy across servlet vs reactive stacks, factor in virtual threads, and standardize timeouts/pooling/resilience/observability so client choice is a deliberate architectural decision.
## The two fluent clients Spring offers two clients with similar builder-style APIs but different execution models: - **`RestClient`** — synchronous/blocking (spring-web, since 6.1). - **`WebClient`** — reactive/non-blocking (spring-webflux, returns `Mono`/`Flux`). ## The servlet threading model Spring **MVC** runs on the **servlet** stack: one request is bound to **one thread** for its lifetime (thread-per-request). When that thread makes an outbound HTTP call and blocks waiting for the response, the thread is parked but **occupied** — it can't serve other requests. The server has a **bounded thread pool** (e.g. Tomcat's default 200), so blocking calls consume from that budget. ### Why RestClient fits - Its blocking model **matches** thread-per-request — no impedance mismatch. - **No reactor/WebFlux dependency** dragged in. - **Simple debugging**: linear stack traces, straightforward exceptions (`HttpClientErrorException`, etc.), easy `try/catch`. - Reuses familiar `ClientHttpRequestFactory` + `HttpMessageConverter` infrastructure. ### What WebClient buys you - **Non-blocking I/O**: many concurrent outbound calls without one-thread-each; a handful of event-loop threads handle thousands of in-flight requests. - **Streaming + backpressure** via `Flux` for large or chunked responses. - Natural **parallel fan-out** with `Mono.zip`/`Flux.merge`. ### The trap: `WebClient.block()` Using WebClient in a servlet app and calling `.block()` gives you the **worst of both**: you still block the servlet thread, plus you pay reactor scheduling overhead and risk subtle issues (blocking on the event loop, harder stack traces). If you're going to block, RestClient is the cleaner tool. WebClient earns its keep only when you actually stay non-blocking. ## Decision guide | Situation | Choose | |---|---| | Ordinary blocking call in Spring MVC | **RestClient** | | Migrating off RestTemplate, still blocking | **RestClient** | | Large concurrent fan-out where thread-per-call would exhaust the pool | **WebClient** (non-blocking) | | Streaming / SSE / backpressure needs | **WebClient** | | Already a WebFlux (reactive) application | **WebClient** | ## Concurrency implications of choosing RestClient - Every in-flight outbound call **holds a servlet worker thread**. Under load, slow upstreams can **exhaust the thread pool** and stall the whole app. - **Mitigations are mandatory**: set connect + read **timeouts**, use a **bounded, pooled** `ClientHttpRequestFactory` (Apache/Jetty), and add **resilience** (retries with caps, circuit breakers, bulkheads) so one bad dependency can't cascade. - For **parallel** blocking calls, you can still fan out across an `ExecutorService`/virtual threads — and **JDK virtual threads (Loom)** substantially reduce the cost of blocking calls, making RestClient attractive even for higher concurrency without going reactive. ## Gotchas - Don't adopt WebFlux/WebClient solely for the nicer API — RestClient already gives you that fluency synchronously. - Mixing a reactive client into an otherwise blocking codebase adds cognitive and operational cost (schedulers, context propagation) for little gain unless you're truly non-blocking end-to-end. - Virtual threads change the calculus: blocking clients scale far better than before, weakening the historical case for WebClient-just-for-concurrency.
- Why is calling WebClient.block() in a servlet app considered an anti-pattern?You still block the servlet thread (no concurrency benefit) but pay reactor scheduling overhead and get harder debugging/stack traces. If you must block, RestClient is simpler and cheaper.
- How do JDK virtual threads change the RestClient-vs-WebClient decision?Virtual threads make blocking calls cheap (a parked virtual thread doesn't tie up a platform thread), so RestClient scales to high concurrency without going reactive, weakening WebClient's concurrency argument.
- What must you configure to keep RestClient from exhausting the servlet thread pool?Mandatory connect/read timeouts, a bounded pooled ClientHttpRequestFactory, and resilience patterns (retry caps, circuit breakers, bulkheads) so a slow upstream can't stall all worker threads.
saying these in an interview costs you the question
- Adopting WebClient in a blocking app just for the fluent API (RestClient already provides it)
- Claiming WebClient.block() gives concurrency benefits in a servlet app
- Ignoring thread-pool exhaustion risk from unbounded/untimed blocking calls
- Saying RestClient can do non-blocking/streaming I/O