How does @LoadBalanced actually work under the hood for RestTemplate versus WebClient?
answer
- RestTemplate -> LoadBalancerInterceptor (blocking)
- WebClient.Builder -> ReactorLoadBalancerExchangeFilterFunction
- BlockingLoadBalancerClient vs reactive choose()
- both go through LoadBalancerClientFactory
- annotate builder, build from it
basics
~20 sFor RestTemplate, @LoadBalanced adds a LoadBalancerInterceptor to the interceptor chain. For WebClient, it adds a load-balancing ExchangeFilterFunction to the builder. Both intercept the request, resolve the service id to an instance, and rewrite the URL.
solid answer
~40 s@LoadBalanced is a qualifier. SCLB auto-configuration finds every bean carrying it and augments it. For a blocking RestTemplate it injects a LoadBalancerInterceptor (or RetryLoadBalancerInterceptor if retries are enabled) into the ClientHttpRequestInterceptor list; that interceptor calls a LoadBalancerClient (BlockingLoadBalancerClient) to choose an instance and reconstructs the URI. For a reactive WebClient.Builder it adds a ReactorLoadBalancerExchangeFilterFunction (or the Deferring variant) as an ExchangeFilterFunction; it non-blockingly subscribes to the ReactorLoadBalancer, picks an instance, and rewrites the request. Both paths funnel through LoadBalancerClientFactory to get the per-service ReactorLoadBalancer and its ServiceInstanceListSupplier. Key difference: RestTemplate uses a blocking interceptor and BlockingLoadBalancerClient; WebClient uses a reactive filter that never blocks. You annotate the WebClient.Builder, not the built WebClient.
code
java · 27 lines@Configuration
class WebClientConfig {
// Annotate the BUILDER, not the built client.
@Bean
@LoadBalanced
WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
@Service
class PaymentClient {
private final WebClient client;
PaymentClient(@LoadBalanced WebClient.Builder builder) {
// Must build from the annotated builder for the LB filter to apply.
this.client = builder.baseUrl("http://payment-service").build();
}
Mono<Receipt> charge(ChargeRequest req) {
return client.post().uri("/charge")
.bodyValue(req)
.retrieve()
.bodyToMono(Receipt.class);
}
}go deeper
Know RestTemplate uses an interceptor and WebClient uses a filter; annotate the bean/builder.
Name LoadBalancerInterceptor and ReactorLoadBalancerExchangeFilterFunction and that WebClient annotates the builder.
Explain the shared LoadBalancerClientFactory core and the retry interceptor swap.
Reason about blocking-vs-reactive implications for timeouts, retries, and never blocking the reactive choose path.
**Same idea, two integration points.** `@LoadBalanced` is a Spring `@Qualifier`. SCLB's auto-config uses a `BeanPostProcessor`/configurer to find beans annotated with it and enrich them. The mechanism differs by client type because RestTemplate is blocking and WebClient is reactive. **RestTemplate (blocking).** RestTemplate exposes a list of `ClientHttpRequestInterceptor`. SCLB's `LoadBalancerInterceptorConfig` registers a `LoadBalancerInterceptor` and a `RestTemplateCustomizer` that adds it to every `@LoadBalanced` RestTemplate. On each call the interceptor: 1. Reads the service id from the request URI's host. 2. Delegates to a `LoadBalancerClient` — the blocking implementation is `BlockingLoadBalancerClient`. 3. `choose(serviceId)` returns a `ServiceInstance`; `reconstructURI(instance, originalUri)` swaps the host:port. 4. Execution continues to the real instance. If `spring.cloud.loadbalancer.retry.enabled=true` (and Spring Retry is present), SCLB uses `RetryLoadBalancerInterceptor` instead, which can retry on the same or next instance per `LoadBalancerRetryPolicy`. **WebClient (reactive).** You annotate a `WebClient.Builder`: ```java @Bean @LoadBalanced WebClient.Builder lbWebClientBuilder() { return WebClient.builder(); } ``` SCLB adds a `ReactorLoadBalancerExchangeFilterFunction` (or `DeferringLoadBalancerExchangeFilterFunction`, which lazily resolves the filter so ordering with other filters works) as an `ExchangeFilterFunction`. On each exchange it: 1. Extracts the service id from the request URL. 2. Subscribes to the service's `ReactorLoadBalancer.choose(request)` — a `Publisher<Response<ServiceInstance>>` — without blocking. 3. Rewrites the request URL to the chosen instance and proceeds down the filter chain. Because it's a filter on the *builder*, you must build the WebClient from the annotated builder; a WebClient created some other way won't be load-balanced. **Shared core.** Both paths obtain the per-service machinery from `LoadBalancerClientFactory` (a `ReactiveLoadBalancer.Factory<ServiceInstance>`), which creates an isolated child `ApplicationContext` per service id. That child context holds the service's `ReactorLoadBalancer` and `ServiceInstanceListSupplier`. This is how per-service customization works. **Gotchas.** - Annotate the *builder* for WebClient and the *template* for RestTemplate — not the calling code. - If you have both load-balanced and plain clients, disambiguate injection with `@LoadBalanced`/`@Qualifier`; injecting a bare `WebClient.Builder` may grab the wrong one. - `WebClient` created via `WebClient.create()` bypasses the filter — always build from the injected `@LoadBalanced WebClient.Builder`. - For blocking apps, the choose path is synchronous; for reactive apps it stays on the event loop (never `.block()` it yourself). **When it matters.** Understanding the interceptor-vs-filter split explains why retries, timeouts, and custom instance selection are configured differently for blocking vs reactive stacks, and why forgetting to build from the annotated builder silently disables balancing.
- Why must you build the WebClient from the @LoadBalanced builder rather than WebClient.create()?The load-balancing ExchangeFilterFunction is attached to the annotated builder. WebClient.create() or an unannotated builder produces a client without that filter, so service-id URLs won't be resolved and requests fail.
- What changes when spring.cloud.loadbalancer.retry.enabled is true for RestTemplate?SCLB swaps LoadBalancerInterceptor for RetryLoadBalancerInterceptor (requires Spring Retry). It retries per LoadBalancerRetryPolicy, optionally on the next instance, controlled by retry properties like max-retries-on-same/next-service-instance.
saying these in an interview costs you the question
- Annotating the built WebClient or WebClient.create() and expecting balancing
- Claiming WebClient uses a blocking interceptor
- Not knowing RestTemplate uses a ClientHttpRequestInterceptor
- Thinking each client type has totally separate LB logic (they share LoadBalancerClientFactory)