Implementing Client-Side Load Balancing with Ribbon, Eureka, and RestTemplate

Ribbon operates as a client-side load balancer that distributes incoming requests across multiple service instances within the same JVM proces. This approach shifts routing logic from network infrastructure to the consumer application itself.

Begin by establishing provider services that register with an Eureka discovery server. A representative controller exposes health/status endpoints:

@RestController
public class ServiceProvider {
    @Value("${server.port}")
    private String nodePort;

    @GetMapping("/status")
    public String checkStatus(@RequestParam("id") String identifier) {
        return "Response from node [" + nodePort + "] | ID: " + identifier;
    }
}

Launch two distinct instances of this provider on ports 9001 and 9002. Configure both to point to the Eureka registry at http://localhost:8761/eureka/. Verify successful registration via the dashboard.

Next, create a consumer module. Add dependencies to your build configuration. Including spring-cloud-starter-netflix-eureka-client automatically resolves the Ribbon library alongside standard web starters.

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Define application properties to bind the consumer to the discovery registry and assign it a dedicated port.

server:
  port: 9010
spring:
  application:
    name: gateway-consumer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

The main application entry point remains standard. Focus shifts to enabling declarative load balancing within the dependency injection container. Create a configuration class that registers a RestTemplate bean annotated with @LoadBalanced. This annotation injects Ribbon's interceptor, allowing the template to resolve logical service names instead of physical addresses.

@Configuration
public class ClientBalanceConfig {
    @Bean
    @LoadBalanced
    public RestTemplate createBalancedRestTemplate() {
        return new RestTemplate();
    }
}

Develop a service layer component to handle outbound calls. Notice that the target URL references the logical Spring application name (gateway-provider) rather than a hardcoded hostname or IP address.

@Service
public class RequestDispatcher {
    private final RestTemplate balancedClient;

    public RequestDispatcher(RestTemplate balancedClient) {
        this.balancedClient = balancedClient;
    }

    public String dispatchRequest(String userId) {
        String url = "http://gateway-provider/status?id=" + userId;
        return balancedClient.getForObject(url, String.class);
    }
}

Expose a external endpoint to trigger these internal calls.

@RestController
public class DispatcherController {
    private final RequestDispatcher dispatcher;

    public DispatcherController(RequestDispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    @GetMapping("/forward")
    public String handleForward(@RequestParam(value = "uid", defaultValue = "default-user") String uid) {
        return dispatcher.dispatchRequest(uid);
    }
}

After launching the consumer application, repeatedly hitting http://localhost:9010/forward?uid=test123 will alternate responses between the backend nodes running on ports 9001 and 9002. This confirms active client-side routing.

For scenarios requiring explicit control over instance selection, Ribbon exposes the LoadBalancerClient interface directly. Inject this interface into a controller to retrieve specific node details before making a request.

@RestController
public class AdvancedController {
    private final LoadBalancerClient lbClient;

    public AdvancedController(LoadBalancerClient lbClient) {
        this.lbClient = lbClient;
    }

    @GetMapping("/discover")
    public String queryNode() {
        ServiceInstance selected = lbClient.choose("gateway-provider");
        return String.format("Routing to %s:%d", selected.getHost(), selected.getPort());
    }
}

Repeated invocations demonstrate the automatic rotation handled by the underlying strategy pattern. By default, LoadBalancerClient caches the service registry obtained from Eureka. To bypass service discovery entirely, you can configure a static server list.

Disable Eureka integration in the configuration and define a custom server array under a designated prefix. In this example, traffic will be routed between two external domains without relying on a registry.

server:
  port: 9020
ribbon:
  eureka:
    enabled: false
my-static-service:
  ribbon:
    listOfServers: https://api.external-one.com,https://api.external-two.com

Map the LoadBalancerClient to this custom key just as before.

@RestController
public class StaticRouteController {
    private final LoadBalancerClient lbClient;

    public StaticRouteController(LoadBalancerClient lbClient) {
        this.lbClient = lbClient;
    }

    @GetMapping("/static-route")
    public String routeStatic() {
        ServiceInstance target = lbClient.choose("my-static-service");
        return String.format("Resolved target -> %s:%d", target.getHost(), target.getPort());
    }
}

Querying this endpoint will cycle through the predefined URLs. This approach highlights Ribbon's flexibility: it seamlessly integrates with dynamic registries like Eureka, or operates independently against explicitly defined host lists, always delegating selection logic to the LoadBalancerClient implementation.

Tags: Spring Cloud Ribbon Load Balancing Eureka RestTemplate

Posted on Thu, 07 May 2026 01:29:44 +0000 by Fawkman