skip to content

How does @LoadBalanced actually work under the hood for RestTemplate versus WebClient?

level: middleimportance: should knowfreq 42%

answer

  1. RestTemplate -> LoadBalancerInterceptor (blocking)
  2. WebClient.Builder -> ReactorLoadBalancerExchangeFilterFunction
  3. BlockingLoadBalancerClient vs reactive choose()
  4. both go through LoadBalancerClientFactory
  5. annotate builder, build from it

basics

~20 s

For 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
java
@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

for a junior

Know RestTemplate uses an interceptor and WebClient uses a filter; annotate the bean/builder.

for a middle

Name LoadBalancerInterceptor and ReactorLoadBalancerExchangeFilterFunction and that WebClient annotates the builder.

for a senior

Explain the shared LoadBalancerClientFactory core and the retry interceptor swap.

for a principal

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)

context