Skip to content

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 ​

MechanismAnnotationResolved URLLoad BalancedSource
Service ID@CoApi(serviceId = "svc")http://svcYesCoApi.kt
LB Protocol@CoApi(baseUrl = "lb://svc")http://svcYesCoApi.kt
Annotation@CoApi @LoadBalancedEmptyYesLoadBalanced.kt
Propertiescoapi.clients.<name>.load-balanced=true/falsePer propertiestrue→Yes, false→NoCoApiProperties.kt
Direct URL@CoApi(baseUrl = "http://...")As specifiedNoCoApi.kt

URL Resolution Flow ​

When toCoApiDefinition() parses the annotation, it resolves the base URL and determines load balancing:

mermaid
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 --> M

The resolution logic in CoApiDefinition.kt:70-97:

InputResolved URLloadBalanced
@CoApi(baseUrl = "lb://order-service")http://order-servicetrue
@CoApi(serviceId = "order-service")http://order-servicetrue
@CoApi @LoadBalanced"" (empty)true
@CoApi(baseUrl = "\${github.url}")resolved valuefalse

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():

mermaid
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
    end

Precedence (CoApiDefinition.kt:156):

PrioritySourceEffect
1 (highest)coapi.clients.<name>.load-balancedOverride to the configured value (true enables, false disables)
2coapi.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 annotationDefault 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:

mermaid
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)
    end

The 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:

mermaid
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)
    end

The 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:

yaml
coapi:
  clients:
    ServiceApiClientUseFilterBeanName:
      reactive:
        filter:
          names:
            - loadBalancerExchangeFilterFunction
    ServiceApiClientUseFilterType:
      reactive:
        filter:
          types:
            - org.springframework.cloud.client.loadbalancer.reactive.LoadBalancedExchangeFilterFunction
PropertyTypeApplies ToSource
coapi.clients.<name>.reactive.filter.namesBean namesWebClient (reactive)CoApiProperties.kt
coapi.clients.<name>.reactive.filter.typesClass typesWebClient (reactive)CoApiProperties.kt
coapi.clients.<name>.sync.interceptor.namesBean namesRestClient (sync)CoApiProperties.kt
coapi.clients.<name>.sync.interceptor.typesClass typesRestClient (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:

yaml
spring:
  cloud:
    discovery:
      client:
        simple:
          instances:
            github-service:
              - host: api.github.com
                secure: true
                port: 443
            provider-service:
              - host: localhost
                port: 8010

Requirements ​

RequirementHow
spring-cloud-starter-loadbalancer on classpathGradle/Maven dependency
Service instances registeredSpring Cloud DiscoveryClient or SimpleDiscoveryClient
Load balancing enabled in CoApiserviceId, lb://, @LoadBalanced, or property

References ​

  1. CoApi.kt — api/src/main/kotlin/me/ahoo/coapi/api/CoApi.kt
  2. LoadBalanced.kt — api/src/main/kotlin/me/ahoo/coapi/api/LoadBalanced.kt
  3. CoApiDefinition.kt — spring/src/main/kotlin/me/ahoo/coapi/spring/CoApiDefinition.kt
  4. ClientProperties.kt — spring/src/main/kotlin/me/ahoo/coapi/spring/client/ClientProperties.kt
  5. WebClientFactoryBean.kt — spring/src/main/kotlin/me/ahoo/coapi/spring/client/reactive/WebClientFactoryBean.kt
  6. RestClientFactoryBean.kt — spring/src/main/kotlin/me/ahoo/coapi/spring/client/sync/RestClientFactoryBean.kt
  7. CoApiProperties.kt — spring-boot-starter/src/main/kotlin/.../CoApiProperties.kt
  8. consumer application.yaml — example/example-consumer-server/src/main/resources/application.yaml