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 LoadBalancerInterceptor (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 | Per properties | Yes | 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 bean creation time, AbstractHttpClientFactoryBean.loadBalanced() applies a precedence order:
sequenceDiagram
autonumber
participant FB as AbstractHttpClientFactoryBean
participant Props as ClientProperties
participant Def as CoApiDefinition
FB->>Props: getLoadBalancedFromProperties(name)
alt Properties has load-balanced value
Props-->>FB: non-null Boolean
FB->>FB: return true
else Properties has baseUrl
FB->>Props: getBaseUrlFromProperties(name)
Props-->>FB: non-blank URL
FB->>FB: return false (direct URL overrides)
else No properties override
FB->>Def: definition.loadBalanced
FB->>FB: return annotation-determined value
endPrecedence (AbstractHttpClientFactoryBean.kt:42-56):
| Priority | Source | Effect |
|---|---|---|
| 1 (highest) | coapi.clients.<name>.load-balanced | Override to true |
| 2 | coapi.clients.<name>.base-url (non-blank) | Forces non-load-balanced |
| 3 (lowest) | @CoApi / @LoadBalanced annotation | Default from annotation |
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: loadBalanced() → true
FB->>Builder: customize(definition, builder)
Builder->>Builder: builder.filters { ... }
Builder->>Builder: check: any existing LoadBalancedExchangeFilterFunction?
alt Already present
Builder->>Builder: skip
else Not present
Builder->>CTX: getBean(LoadBalancedExchangeFilterFunction)
CTX-->>LB: filter function
Builder->>Builder: filters.add(LB)
endThe LoadBalancedWebClientBuilderCustomizer inner class (WebClientFactoryBean.kt:34-43) checks for duplicates before adding, ensuring idempotency.
RestClient Load Balancing
For the synchronous stack, RestClientFactoryBean adds LoadBalancerInterceptor:
sequenceDiagram
autonumber
participant FB as RestClientFactoryBean
participant CTX as ApplicationContext
participant Builder as RestClient.Builder
participant LB as LoadBalancerInterceptor
FB->>FB: loadBalanced() → true
FB->>Builder: customize(definition, builder)
Builder->>Builder: builder.requestInterceptors { ... }
Builder->>Builder: check: any existing load balancer interceptor?
alt Already present
Builder->>Builder: skip
else Not present
Builder->>CTX: getBean(LoadBalancerInterceptor)
CTX-->>LB: interceptor
Builder->>Builder: interceptors.add(LB)
endPer-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 |
Filter resolution in AbstractWebClientFactoryBean.kt resolves bean names and types from ApplicationContext.
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 - AbstractHttpClientFactoryBean.kt —
spring/src/main/kotlin/me/ahoo/coapi/spring/client/AbstractHttpClientFactoryBean.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