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 LoadBalancerInterceptor (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=truePer propertiesYesCoApiProperties.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 bean creation time, AbstractHttpClientFactoryBean.loadBalanced() applies a precedence order:

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

Precedence (AbstractHttpClientFactoryBean.kt:42-56):

PrioritySourceEffect
1 (highest)coapi.clients.<name>.load-balancedOverride to true
2coapi.clients.<name>.base-url (non-blank)Forces non-load-balanced
3 (lowest)@CoApi / @LoadBalanced annotationDefault from annotation

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

The LoadBalancedWebClientBuilderCustomizer inner class (WebClientFactoryBean.kt:34-43) checks for duplicates before adding, ensuring idempotency.

RestClient Load Balancing

For the synchronous stack, RestClientFactoryBean adds LoadBalancerInterceptor:

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

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

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:

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.ktapi/src/main/kotlin/me/ahoo/coapi/api/CoApi.kt
  2. LoadBalanced.ktapi/src/main/kotlin/me/ahoo/coapi/api/LoadBalanced.kt
  3. CoApiDefinition.ktspring/src/main/kotlin/me/ahoo/coapi/spring/CoApiDefinition.kt
  4. AbstractHttpClientFactoryBean.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/AbstractHttpClientFactoryBean.kt
  5. WebClientFactoryBean.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/reactive/WebClientFactoryBean.kt
  6. RestClientFactoryBean.ktspring/src/main/kotlin/me/ahoo/coapi/spring/client/sync/RestClientFactoryBean.kt
  7. CoApiProperties.ktspring-boot-starter/src/main/kotlin/.../CoApiProperties.kt
  8. consumer application.yamlexample/example-consumer-server/src/main/resources/application.yaml