Explain the ReactorLoadBalancer / ReactiveLoadBalancer.Factory abstraction and how per-service configuration isolation works in Spring Cloud LoadBalancer.
answer
- ReactiveLoadBalancer.choose() -> Publisher<Response>
- LoadBalancerClientFactory = ReactiveLoadBalancer.Factory
- NamedContextFactory: one child context per service id
- @LoadBalancerClient imports config into that child only
- config in main scan -> parent bean -> leaks globally
basics
~10 sReactorLoadBalancer<ServiceInstance> is the interface whose choose() picks an instance. LoadBalancerClientFactory (a ReactiveLoadBalancer.Factory) builds a separate child ApplicationContext per service, so each service can have its own load balancer, supplier, and config without affecting others.
solid answer
~40 sThe choosing contract is ReactiveLoadBalancer<T> with Publisher<Response<T>> choose(Request request); ReactorLoadBalancer<T> is the Reactor-typed specialization used with ServiceInstance. Concrete strategies are RoundRobinLoadBalancer (default) and RandomLoadBalancer. These are created and cached by LoadBalancerClientFactory, which implements ReactiveLoadBalancer.Factory<ServiceInstance>. Its key trick: it uses a NamedContextFactory to spin up an isolated child ApplicationContext per service id. Each child inherits defaults (LoadBalancerClientConfiguration) but can be overridden via @LoadBalancerClient(value, configuration) — the config class is imported only into that service's child context, so a RandomLoadBalancer or custom ServiceInstanceListSupplier for order-service doesn't leak to payment-service. This is exactly why custom LB config classes must live outside the main @ComponentScan: scanning them puts beans in the parent context and makes them global defaults. @LoadBalancerClients(defaultConfiguration=...) intentionally sets a shared default.
code
java · 21 lines// Interface (simplified) — the choosing contract:
public interface ReactiveLoadBalancer<T> {
Publisher<Response<T>> choose(Request request);
}
public interface ReactorLoadBalancer<T> extends ReactiveLoadBalancer<T> { }
// Per-service override, imported ONLY into order-service's child context:
public class OrderLbConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> orderLoadBalancer(
Environment env, LoadBalancerClientFactory factory) {
String serviceId = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
factory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
serviceId);
}
}
@Configuration
@LoadBalancerClient(value = "order-service", configuration = OrderLbConfig.class)
class LbClientsConfig { } // OrderLbConfig itself must be OUTSIDE the main @ComponentScango deeper
Just know ReactorLoadBalancer picks an instance; RoundRobin is the default implementation.
Understand @LoadBalancerClient applies config to one service and there's a factory creating balancers.
Explain the per-service child context isolation and why config placement matters.
Design per-dependency policies (sticky/zone/weighted/canary) via child-context configs, handle Ribbon migration, and weigh the cost of per-service context multiplication.
**The contracts.** At the center is `ReactiveLoadBalancer<T>` with one method: `Publisher<Response<T>> choose(Request request)`. `ReactorLoadBalancer<T>` narrows the return to Reactor types and is the interface you implement/override; for load balancing `T` is `ServiceInstance`. A `Response<ServiceInstance>` wraps the chosen instance (and supports lifecycle callbacks like `onComplete` used for stats/sticky sessions). Built-in implementations: `RoundRobinLoadBalancer` (default) and `RandomLoadBalancer`. **The factory.** `LoadBalancerClientFactory` implements `ReactiveLoadBalancer.Factory<ServiceInstance>`. Callers (the RestTemplate interceptor / WebClient filter) ask the factory: `getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class)` (and similarly for the `ServiceInstanceListSupplier`). The factory lazily creates and caches these per service id. **Per-service child contexts — the core design.** `LoadBalancerClientFactory` extends Spring Cloud's `NamedContextFactory`. For each service id it builds a **separate child `ApplicationContext`**, a child of the main context. Into each child it imports: 1. The default config `LoadBalancerClientConfiguration` (which supplies the default `RoundRobinLoadBalancer` and the default caching `ServiceInstanceListSupplier`). 2. Any per-service overrides you registered. Because each service gets its own context, `order-service` and `payment-service` can run entirely different balancers, suppliers, health-check settings, and caches without interference. This is the modern analogue of Ribbon's per-client `IClientConfig`. **Registering overrides.** - `@LoadBalancerClient(value = "order-service", configuration = OrderLbConfig.class)` imports `OrderLbConfig` into *only* the `order-service` child context. - `@LoadBalancerClients({ @LoadBalancerClient(...), ... })` registers several. - `@LoadBalancerClients(defaultConfiguration = CommonLbConfig.class)` sets a config imported into *every* service's child context — a deliberate global default. **Why config-class placement is a classic trap.** The override config classes are meant to be imported into child contexts, *not* component-scanned into the parent. If you annotate one `@Configuration` and it sits under your `@SpringBootApplication` package, the main `@ComponentScan` picks it up and registers its beans in the **parent** context. A bean in the parent is visible to *all* child contexts (children inherit parent beans), so your 'just for order-service' `RandomLoadBalancer` silently becomes the default everywhere — or worse, causes bean conflicts. Rule: keep per-client LB config classes in a package outside the main scan (or otherwise exclude them), and wire them only through `@LoadBalancerClient`. **Building a custom balancer correctly.** Because the balancer lives in the child context, it must obtain *that service's* supplier lazily: ```java new RandomLoadBalancer( factory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId); ``` `getLazyProvider` returns an `ObjectProvider` resolved from the child context, and `serviceId` is read from `LoadBalancerClientFactory.PROPERTY_NAME` in the environment. **Relationship to discovery vs standalone.** SCLB can run without a `DiscoveryClient`: you can supply a static `ServiceInstanceListSupplier` (fixed host list) for testing or simple setups. The factory/child-context machinery is the same; only the base supplier changes. **Design implications (principal lens).** - The child-context pattern gives strong isolation but means beans you expect to be singletons exist once per service context — watch for accidental heavy beans multiplied per service. - Cross-cutting concerns (metrics, tracing) hook via `LoadBalancerLifecycle` beans registered in the (child or parent) context; `onStart`/`onComplete` callbacks let you record chosen-instance stats. - Migrating from Ribbon: `IRule` → custom `ReactorLoadBalancer`; `ServerList` → `ServiceInstanceListSupplier`; `@RibbonClient` → `@LoadBalancerClient`; `IClientConfig`/`ribbon.*` props → `spring.cloud.loadbalancer.*` + per-client config classes. **When to reach for custom configs.** Different SLAs per dependency (e.g., sticky sessions for one service, zone-preference for another, weighted canary routing for a third) — each expressed as a per-service child-context configuration, which is exactly what this abstraction is built to support.
- How does SCLB keep order-service's custom balancer from affecting payment-service?LoadBalancerClientFactory (a NamedContextFactory) creates a separate child ApplicationContext per service id. The @LoadBalancerClient config is imported only into that service's child context, so overrides are isolated per service.
- Map the main Ribbon concepts to their SCLB equivalents.IRule -> custom ReactorLoadBalancer; ServerList -> ServiceInstanceListSupplier; @RibbonClient -> @LoadBalancerClient; IClientConfig/ribbon.* properties -> spring.cloud.loadbalancer.* plus per-client configuration classes.
- Can SCLB work without a DiscoveryClient?Yes. You can register a static ServiceInstanceListSupplier with a fixed instance list. The factory/child-context and balancer machinery are unchanged; only the base supplier differs.
saying these in an interview costs you the question
- Not knowing choose() returns a Publisher<Response<ServiceInstance>>
- Thinking there's one shared load-balancer bean for all services
- Component-scanning per-client config and expecting isolation
- Confusing the ReactorLoadBalancer (selection) with the ServiceInstanceListSupplier (candidate list)
- Assuming SCLB requires a discovery client