Client-Side Load Balancing
Overview
In microservice architectures, services need to call other services without hardcoding hostnames. CoApi integrates with Spring Cloud LoadBalancer to provide client-side load balancing: the HTTP client itself selects which service instance to call. This eliminates the need for an external load balancer and gives the application direct control over instance selection, retries, and circuit breaking.
CoApi provides three ways to opt into load balancing, all resolving to the same mechanism: a LoadBalancedExchangeFilterFunction (reactive) or a BlockingLoadBalancerInterceptor (sync) is added to the HTTP client's filter/interceptor chain.
At a Glance
| Mechanism | Annotation | Resolved URL | Load Balanced | Source |
|---|---|---|---|---|
| Service ID | @CoApi(serviceId = "svc") | http://svc | Yes | CoApi.kt |
| LB Protocol | @CoApi(baseUrl = "lb://svc") | http://svc | Yes | CoApi.kt |
| Annotation | @CoApi @LoadBalanced | Empty | Yes | LoadBalanced.kt |
| Properties | coapi.clients.<name>.load-balanced=true/false | Per properties | true→Yes, false→No | CoApiProperties.kt |
| Direct URL | @CoApi(baseUrl = "http://...") | As specified | No | CoApi.kt |
URL Resolution Flow
When toCoApiDefinition() parses the annotation, it resolves the base URL and determines load balancing:
flowchart TD
A["@CoApi annotation"] --> B{baseUrl is not blank?}
B -->|Yes| C["Resolve ${} placeholders"]
B -->|No| D{serviceId is not blank?}
D -->|Yes| E["lb:// + serviceId"]
D -->|No| F[Empty string]
C --> G{Starts with lb:// ?}
E --> G
G -->|Yes| H["Strip lb:// → http://<br>loadBalanced = true"]
G -->|No| I["Use URL as-is<br>loadBalanced = false"]
F --> J{"@LoadBalanced present?"}
J -->|Yes| K["loadBalanced = true"]
J -->|No| L["loadBalanced = false"]
H --> M[CoApiDefinition]
I --> M
K --> M
L --> MThe resolution logic in CoApiDefinition.kt:70-97:
| Input | Resolved URL | loadBalanced |
|---|---|---|
@CoApi(baseUrl = "lb://order-service") | http://order-service | true |
@CoApi(serviceId = "order-service") | http://order-service | true |
@CoApi @LoadBalanced | "" (empty) | true |
@CoApi(baseUrl = "\${github.url}") | resolved value | false |
Runtime Load Balancing Decision
At client creation time, the factory bean resolves the effective definition: ClientProperties.resolve(definition) applies the coapi.clients.<name>.* overrides through the single rule in CoApiDefinition.withOverrides():
sequenceDiagram
autonumber
participant FB as WebClientFactoryBean / RestClientFactoryBean
participant Props as ClientProperties
participant Def as CoApiDefinition
FB->>Props: resolve(definition)
Props->>Def: withOverrides(base-url, load-balanced)
alt load-balanced configured
Def-->>FB: configured value (true/false)
else base-url configured
Def-->>FB: true only for an lb:// URL (rewritten to http://)
else no override
Def-->>FB: annotation-determined value
endPrecedence (CoApiDefinition.kt:156):
| Priority | Source | Effect |
|---|---|---|
| 1 (highest) | coapi.clients.<name>.load-balanced | Override to the configured value (true enables, false disables) |
| 2 | coapi.clients.<name>.base-url (non-blank) | Load balanced only for an lb:// URL (since v2.3.0); a plain http(s):// URL disables it |
| 3 (lowest) | @CoApi / @LoadBalanced annotation | Default from annotation |
The lb:// scheme is matched case-insensitively (LB:// works too, since v2.3.0). If a client resolves to load balanced but Spring Cloud LoadBalancer is not on the classpath, client creation fails with an actionable message (since v3.0.0) instead of a NoClassDefFoundError.
INFO
Since v2.1.1, an explicit load-balanced: false is respected and disables load balancing — before v2.1.1 any configured value (including false) was treated as true. The <name> in coapi.clients.<name>.* is the @CoApi name attribute, or the interface simple name when no name is set.
WebClient Load Balancing
For the reactive stack, WebClientFactoryBean adds LoadBalancedExchangeFilterFunction:
sequenceDiagram
autonumber
participant FB as WebClientFactoryBean
participant CTX as ApplicationContext
participant Builder as WebClient.Builder
participant LB as LoadBalancedExchangeFilterFunction
FB->>FB: effectiveDefinition().loadBalanced → true
FB->>Builder: LoadBalancedWebClientBuilderCustomizer.customize(definition, builder)
Builder->>Builder: builder.filters { ... }
Builder->>Builder: check: any existing LB filter (LoadBalanced or Deferring)?
alt Already present
Builder->>Builder: skip
else Not present
Builder->>CTX: getBean(LoadBalancedExchangeFilterFunction)
CTX-->>LB: filter function
Builder->>Builder: filters.add(LB)
endThe internal LoadBalancedWebClientBuilderCustomizer (LoadBalancedWebClientBuilderCustomizer.kt:36) is applied only to load-balanced clients and checks for duplicates before adding, ensuring idempotency. The check recognizes both LoadBalancedExchangeFilterFunction and Spring Cloud's DeferringLoadBalancerExchangeFilterFunction (the default wiring on @LoadBalanced WebClient.Builder), so a builder that already carries load balancing is not double-wired.
RestClient Load Balancing
For the synchronous stack, RestClientFactoryBean adds a BlockingLoadBalancerInterceptor:
sequenceDiagram
autonumber
participant FB as RestClientFactoryBean
participant CTX as ApplicationContext
participant Builder as RestClient.Builder
participant LB as BlockingLoadBalancerInterceptor
FB->>FB: effectiveDefinition().loadBalanced → true
FB->>Builder: LoadBalancedRestClientBuilderCustomizer.customize(definition, builder)
Builder->>Builder: builder.requestInterceptors { ... }
Builder->>Builder: check: any existing LB interceptor (Blocking or Deferring)?
alt Already present
Builder->>Builder: skip
else Not present
Builder->>CTX: getBean(BlockingLoadBalancerInterceptor)
CTX-->>LB: interceptor
Builder->>Builder: interceptors.add(LB)
endThe interceptor is resolved by the BlockingLoadBalancerInterceptor interface rather than the concrete LoadBalancerInterceptor class, so it works whether Spring Cloud registers the plain interceptor or the RetryLoadBalancerInterceptor (retry enabled). Like the reactive side, the dedup check recognizes Spring Cloud's DeferringLoadBalancerInterceptor installed on @LoadBalanced RestClient.Builder.
Per-Client Filter & Interceptor Configuration
Beyond load balancing, CoApi supports per-client filter/interceptor chains via YAML properties:
coapi:
clients:
ServiceApiClientUseFilterBeanName:
reactive:
filter:
names:
- loadBalancerExchangeFilterFunction
ServiceApiClientUseFilterType:
reactive:
filter:
types:
- org.springframework.cloud.client.loadbalancer.reactive.LoadBalancedExchangeFilterFunction| Property | Type | Applies To | Source |
|---|---|---|---|
coapi.clients.<name>.reactive.filter.names | Bean names | WebClient (reactive) | CoApiProperties.kt |
coapi.clients.<name>.reactive.filter.types | Class types | WebClient (reactive) | CoApiProperties.kt |
coapi.clients.<name>.sync.interceptor.names | Bean names | RestClient (sync) | CoApiProperties.kt |
coapi.clients.<name>.sync.interceptor.types | Class types | RestClient (sync) | CoApiProperties.kt |
The factory beans resolve these references from the ApplicationContext — all names first, then all types — via AbstractHttpClientFactoryBean.kt. The filters come from ReactiveClientProperties and the interceptors from SyncClientProperties (both implemented by CoApiProperties).
Service Discovery Configuration
CoApi works with any Spring Cloud DiscoveryClient. A simple in-memory configuration for development:
spring:
cloud:
discovery:
client:
simple:
instances:
github-service:
- host: api.github.com
secure: true
port: 443
provider-service:
- host: localhost
port: 8010Requirements
| Requirement | How |
|---|---|
spring-cloud-starter-loadbalancer on classpath | Gradle/Maven dependency |
| Service instances registered | Spring Cloud DiscoveryClient or SimpleDiscoveryClient |
| Load balancing enabled in CoApi | serviceId, lb://, @LoadBalanced, or property |
Related Pages
- Annotations (@CoApi, @LoadBalanced) — annotation parameters and URL resolution
- Client Modes (Reactive & Sync) — WebClient vs RestClient internals
- Customization & Extensibility — customizer SPI and filter chains
- Configuration Reference — all YAML properties
References
- CoApi.kt —
api/src/main/kotlin/me/ahoo/coapi/api/CoApi.kt - LoadBalanced.kt —
api/src/main/kotlin/me/ahoo/coapi/api/LoadBalanced.kt - CoApiDefinition.kt —
spring/src/main/kotlin/me/ahoo/coapi/spring/CoApiDefinition.kt - ClientProperties.kt —
spring/src/main/kotlin/me/ahoo/coapi/spring/client/ClientProperties.kt - WebClientFactoryBean.kt —
spring/src/main/kotlin/me/ahoo/coapi/spring/client/reactive/WebClientFactoryBean.kt - RestClientFactoryBean.kt —
spring/src/main/kotlin/me/ahoo/coapi/spring/client/sync/RestClientFactoryBean.kt - CoApiProperties.kt —
spring-boot-starter/src/main/kotlin/.../CoApiProperties.kt - consumer application.yaml —
example/example-consumer-server/src/main/resources/application.yaml