Client-Side Load Balancing in Spring Boot Microservices Using Spring Cloud LoadBalancer

Introduction

In a microservices architecture, a single service can run multiple instances to handle incoming requests. However, if every request is sent to the same instance, that instance may become overloaded while other instances remain underutilized.

Load Balancing helps distribute requests across multiple service instances, improving resource utilization and helping applications handle increased traffic.

In Spring Boot microservices, Spring Cloud LoadBalancer provides client-side load-balancing capabilities. It works with service discovery mechanisms such as Netflix Eureka to identify available service instances and select an instance for a request.

In this article, we will learn:

  • What load balancing is and why it is required.
  • The difference between server-side and client-side load balancing.
  • How to configure Spring Cloud LoadBalancer with RestTemplate.
  • How Round Robin and Random Load Balancing work.
  • How to configure different algorithms for different services.
  • How to implement a custom load-balancing strategy.
  • How load balancing works with FeignClient.

1. What Is Load Balancing?

Load balancing is the process of distributing incoming requests across multiple instances of a service.

Consider an application with three Product Service instances:

                  +----------------+
                  |  Order Service |
                  +----------------+
                          |
                          v
                 Load Balancing Logic
                          |
              +-----------+-----------+
              |           |           |
              v           v           v
         +---------+ +---------+ +---------+
         | Product | | Product | | Product |
         | Instance| | Instance| | Instance|
         |    1    | |    2    | |    3    |
         +---------+ +---------+ +---------+

Instead of always calling one instance, the load balancer selects an instance according to its configured algorithm.

For example, the Order Service may send one request to Product Service Instance 1, the next to Instance 2, and the next to Instance 3.

This approach helps distribute traffic across the available instances.

2. Types of Load Balancing

There are two common approaches to load balancing in distributed applications: server-side and client-side load balancing.

2.1 Server-Side Load Balancing

In server-side load balancing, a centralized load balancer receives requests and forwards them to the appropriate service instance.

Examples include Nginx and AWS Elastic Load Balancing.

       Client
          |
          v
   +----------------+
   | Load Balancer  |
   +----------------+
       |    |    |
       v    v    v
      S1   S2   S3

The client sends a request to the load balancer, and the load balancer selects the destination instance.

The client does not need to implement the instance-selection algorithm itself.

2.2 Client-Side Load Balancing

In client-side load balancing, the calling application or its client library selects a service instance.

For example, an Order Service needs to invoke the Product Service. It obtains the available Product Service instances through service discovery and uses a load-balancing algorithm to select one.

             +------------------+
             | Eureka Server    |
             | Service Registry |
             +------------------+
                      ^
                      |
                      v
             +------------------+
             |  Order Service   |
             | Client Load      |
             | Balancer         |
             +------------------+
                      |
             Select an instance
                      |
              +-------+-------+
              |       |       |
              v       v       v
             S1      S2      S3

Examples and related technologies include Spring Cloud LoadBalancer, Netflix Ribbon, and service-mesh solutions such as Istio.

Spring Cloud LoadBalancer is the focus of this tutorial.

3. Why Do We Need Client-Side Load Balancing?

Suppose the Order Service uses DiscoveryClient to retrieve Product Service instances.

It can obtain a list of registered instances, but the application must still decide which instance to call.

Consider this code:

List<ServiceInstance> instances =
        discoveryClient.getInstances("product-service");

URI uri = instances.get(0).getUri();

The code selects the first instance in the list.

This demonstrates service discovery, but it does not implement a proper load-balancing strategy. Repeatedly selecting the first instance can cause traffic to concentrate on that instance.

Spring Cloud LoadBalancer provides an abstraction for selecting service instances using a configured algorithm.

Instead of implementing the selection logic manually, the application can delegate the decision to the framework.

4. Setting Up Spring Cloud LoadBalancer

Let’s implement client-side load balancing using the following applications:

  • Eureka Server: Maintains the service registry.
  • Product Service: Runs on multiple instances.
  • Order Service: Calls the Product Service using RestTemplate.

The examples assume that Eureka is already configured and the services register successfully.

Step 1: Add the dependency

Add the following dependency to the Order Service’s pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

Make sure your project uses Spring Cloud dependency management compatible with your Spring Boot version.

For the Eureka-based setup, the Order Service also needs the appropriate Eureka Client dependency.

5. Using RestTemplate Without Load Balancing

Before using Spring Cloud LoadBalancer, let’s look at a basic implementation.

@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    DiscoveryClient discoveryClient;

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/{id}")
    public String callProductAPI(@PathVariable String id) {

        List<ServiceInstance> instances =
                discoveryClient.getInstances("product-service");

        if (instances.isEmpty()) {
            throw new IllegalStateException(
                    "No Product Service instances available");
        }

        URI uri = instances.get(0).getUri();

        return restTemplate.getForObject(
                uri + "/products/" + id,
                String.class
        );
    }
}

In this example:

  1. DiscoveryClient retrieves the registered Product Service instances.
  2. The application selects the first instance.
  3. RestTemplate sends the request to that instance.

The disadvantage is that the application is responsible for choosing the instance.

Now let’s simplify this implementation using Spring Cloud LoadBalancer.

6. Configuring RestTemplate with @LoadBalanced

Spring Cloud provides the @LoadBalanced annotation to configure a client for service-name-based load balancing.

Step 1: Create a Load-Balanced RestTemplate

Create a configuration class:

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class Config {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

The @LoadBalanced annotation marks the RestTemplate bean for integration with Spring Cloud’s client-side load-balancing infrastructure.

When the application makes a request using a logical service name, Spring Cloud can resolve that name through service discovery and select an instance using the configured load balancer.

Step 2: Configure the Order Service

Add the following properties to application.properties:

server.port=8081
spring.application.name=order-service

eureka.client.service-url.defaultZone=http://localhost:8761/eureka

product.service.baseurl=http://product-service

Notice the following configuration:

product.service.baseurl=http://product-service

Here, product-service is the logical service name registered with Eureka, not a physical IP address.

The Eureka Client and Spring Cloud LoadBalancer dependencies allow the application to resolve this logical name to a registered service instance.

Step 3: Call the Product Service

Now update the Order Controller:

@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    RestTemplate restTemplate;

    @Value("${product.service.baseurl}")
    String productBaseURL;

    @GetMapping("/{id}")
    public String callProductAPI(@PathVariable String id) {

        return restTemplate.getForObject(
                productBaseURL + "/products/" + id,
                String.class
        );
    }
}

When a request arrives at the Order Service, the application calls the Product Service using its logical name.

Spring Cloud LoadBalancer selects an instance, and the request is sent to that instance.

The application no longer needs to retrieve the instance list and select an instance manually.

7. How Does @LoadBalanced Work Internally?

Let’s understand the high-level execution flow.

Suppose the Order Service executes:

restTemplate.getForObject(
        "http://product-service/products/101",
        String.class
);

The request contains the logical service name product-service.

The general flow is:

  1. The load-balanced client intercepts the request.
  2. The logical service name is identified.
  3. The configured service-instance supplier obtains the available instances, typically using the discovery client.
  4. Spring Cloud LoadBalancer selects an instance according to its configured algorithm.
  5. The request is sent to the selected instance.

For example, if Eureka has registered three Product Service instances, the load balancer can choose one of those instances for the request.

This makes service-to-service communication more flexible because the caller does not need to know each instance’s physical address.

8. Load-Balancing Algorithms in Spring Cloud LoadBalancer

A load-balancing algorithm determines which service instance receives a request.

The reference tutorial focuses on two built-in strategies:

  • Round Robin
  • Random

8.1 Round Robin Load Balancing

Round Robin selects instances in a rotating sequence.

Suppose three Product Service instances are registered:

  • Instance A
  • Instance B
  • Instance C

Requests are distributed in a sequence similar to the following:

Request Selected instance
Request 1 Instance A
Request 2 Instance B
Request 3 Instance C
Request 4 Instance A
Request 5 Instance B
Request 6 Instance C

Round Robin is the default algorithm in the setup described by the reference tutorial.

It is a useful starting point when service instances have similar capacity and request-processing costs.

However, Round Robin does not automatically guarantee equal resource utilization if requests have different processing times or instances have different capacities.

8.2 Random Load Balancing

Random Load Balancing selects an instance randomly from the available instances.

For example, the selected sequence might be:

Request Selected instance
Request 1 Instance B
Request 2 Instance A
Request 3 Instance B
Request 4 Instance C
Request 5 Instance A

The sequence is not fixed.

Random selection can distribute requests across instances without following a rotating order.

9. Switching from Round Robin to Random Load Balancing

Suppose the default Round Robin algorithm is not suitable for a particular service, and we want to use Random Load Balancing for product-service.

Spring Cloud allows us to configure a load balancer for a specific service ID.

Step 1: Configure @LoadBalancerClient

Add the following annotation to the Order Service application:

@SpringBootApplication
@EnableFeignClients
@LoadBalancerClient(
    name = "product-service",
    configuration = LoadBalancerProductClientConfig.class
)
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(
                OrderServiceApplication.class, args);
    }
}

The important part is:

@LoadBalancerClient(
    name = "product-service",
    configuration = LoadBalancerProductClientConfig.class
)

It associates a custom load-balancer configuration with the product-service service ID.

Step 2: Create the load-balancer configuration

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.LoadBalancerClientFactory;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class LoadBalancerProductClientConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> productClientLoadBalancer(
            LoadBalancerClientFactory factory) {

        return new RandomLoadBalancer(
                factory.getLazyProvider(
                        "product-service",
                        ServiceInstanceListSupplier.class
                ),
                "product-service"
        );
    }
}

The RandomLoadBalancer receives a supplier of service instances and the service ID.

The supplier provides the available Product Service instances, while the load balancer chooses one randomly.

As a result, Random Load Balancing is applied to product-service through this configuration.

Important: Keep service-specific load-balancer configuration outside the main component-scanning package when necessary, so it does not accidentally become the global configuration for every service.

10. Using Different Algorithms for Different Services

Consider an application that communicates with multiple microservices.

We might want:

  • Random Load Balancing for product-service.
  • Round Robin Load Balancing for other services.

Spring Cloud allows a default configuration to be combined with a service-specific configuration.

Step 1: Configure the default and service-specific strategies

@SpringBootApplication
@EnableFeignClients
@LoadBalancerClients(
    defaultConfiguration = LoadBalancerGlobalConfig.class,
    value = {
        @LoadBalancerClient(
            name = "product-service",
            configuration = LoadBalancerProductClientConfig.class
        )
    }
)
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(
                OrderServiceApplication.class, args);
    }
}

Here:

  • defaultConfiguration specifies the default load-balancer configuration.
  • @LoadBalancerClient specifies the configuration for product-service.

Step 2: Configure the default Round Robin algorithm

@Configuration
@ConditionalOnMissingBean(ReactorLoadBalancer.class)
public class LoadBalancerGlobalConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> defaultLoadBalancer(
            LoadBalancerClientFactory factory,
            Environment environment) {

        String serviceId =
                environment.getProperty(
                        LoadBalancerClientFactory.PROPERTY_NAME);

        return new RoundRobinLoadBalancer(
                factory.getLazyProvider(
                        serviceId,
                        ServiceInstanceListSupplier.class
                ),
                serviceId
        );
    }
}

The global configuration uses Round Robin as the default strategy.

The @ConditionalOnMissingBean annotation prevents this configuration from creating another load-balancer bean when a service-specific configuration already supplies one.

Expected behavior

Service Load-balancing algorithm
product-service Random
Other services Round Robin

This pattern allows different services to use different instance-selection strategies.

The exact configuration should be verified against the Spring Cloud version used by your project.

11. Implementing a Custom Load-Balancing Algorithm

Sometimes the built-in algorithms are insufficient for application requirements.

For example, you may want to implement a strategy that chooses instances according to a custom rule.

Spring Cloud LoadBalancer provides the ReactorServiceInstanceLoadBalancer interface for custom reactive instance-selection logic.

A simplified example that selects the first available instance is shown below:

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.beans.factory.ObjectProvider;
import reactor.core.publisher.Mono;

public class MyCustomLoadBalancer
        implements ReactorServiceInstanceLoadBalancer {

    private final ObjectProvider<ServiceInstanceListSupplier>
            serviceInstanceSuppliers;

    private final String serviceId;

    public MyCustomLoadBalancer(
            ObjectProvider<ServiceInstanceListSupplier> suppliers,
            String serviceId) {

        this.serviceInstanceSuppliers = suppliers;
        this.serviceId = serviceId;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {

        return serviceInstanceSuppliers
                .getIfAvailable()
                .get()
                .next()
                .map(instances -> {
                    if (instances == null || instances.isEmpty()) {
                        return new EmptyResponse();
                    }

                    return new DefaultResponse(instances.get(0));
                });
    }
}

This example illustrates the custom selection point, but the first-instance strategy is only a demonstration. It does not provide meaningful load distribution.

In a production implementation, also handle the case where the supplier is unavailable and verify the API signatures against the Spring Cloud version being used.

Register the custom load balancer

The custom implementation can be registered through the service-specific configuration:

@Configuration
public class LoadBalancerProductClientConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> productClientLoadBalancer(
            LoadBalancerClientFactory factory) {

        return new MyCustomLoadBalancer(
                factory.getLazyProvider(
                        "product-service",
                        ServiceInstanceListSupplier.class
                ),
                "product-service"
        );
    }
}

This allows the product-service client to use the custom instance-selection logic.

Possible custom strategies include weighted selection, selection based on application-specific metadata, or other policies supported by the available service-instance information.

Such strategies require additional implementation and testing.

12. How Does FeignClient Work with Load Balancing?

Spring Cloud OpenFeign provides a declarative alternative to RestTemplate.

For example:

@FeignClient(name = "product-service")
public interface ProductClient {

    @GetMapping("/products/{id}")
    String getProductById(@PathVariable("id") String id);
}

The Order Service can invoke the Product Service through this interface:

@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    ProductClient productClient;

    @GetMapping("/{id}")
    public String callProductAPI(@PathVariable String id) {

        return productClient.getProductById(id);
    }
}

When Eureka discovery and Spring Cloud LoadBalancer are configured correctly, the Feign client resolves product-service and selects an instance using the configured load-balancing strategy.

The appropriate Eureka Client, Spring Cloud OpenFeign, and Spring Cloud LoadBalancer dependencies must be included.

Unlike the manual DiscoveryClient approach, the calling code does not need to retrieve an instance list and choose an instance itself.

This makes FeignClient convenient for service-to-service communication.

13. Spring Cloud LoadBalancer vs. Netflix Ribbon

Both Spring Cloud LoadBalancer and Netflix Ribbon provide client-side load-balancing capabilities, but they belong to different generations of the Spring Cloud ecosystem.

Spring Cloud LoadBalancer is the modern Spring Cloud solution used in current Spring Cloud applications.

Netflix Ribbon is an older client-side load-balancing library that has been placed in maintenance mode and is no longer the recommended choice for new Spring Cloud projects.

For new Spring Boot microservices, prefer Spring Cloud LoadBalancer with a compatible Spring Cloud release train.

14. Key Takeaways

Let’s summarize the important concepts:

  1. Load balancing distributes requests across multiple service instances.
  2. Server-side load balancing uses a centralized component to select the destination.
  3. Client-side load balancing selects instances from the calling application or its client library.
  4. Spring Cloud LoadBalancer integrates with service discovery.
  5. @LoadBalanced enables service-name-based load balancing for supported clients such as RestTemplate.
  6. Round Robin is the default strategy in the setup discussed here.
  7. Random Load Balancing selects an instance randomly.
  8. @LoadBalancerClient allows service-specific load-balancer configuration.
  9. Custom algorithms can be implemented using ReactorServiceInstanceLoadBalancer.
  10. FeignClient can work with Spring Cloud LoadBalancer when the required dependencies and configuration are present.

15. Frequently Asked Interview Questions

Q1. What is client-side load balancing?

Client-side load balancing means the calling application or its client library selects the service instance that will receive a request.

Q2. What is the purpose of @LoadBalanced?

It marks a client bean, such as RestTemplate, for integration with Spring Cloud’s load-balancing infrastructure.

Q3. What is the default load-balancing algorithm in Spring Cloud LoadBalancer?

Round Robin is the default algorithm in the configuration described in this article.

Q4. What is the difference between Round Robin and Random Load Balancing?

Round Robin selects instances in a rotating sequence, whereas Random selects an instance randomly.

Q5. Can we configure different algorithms for different microservices?

Yes. Service-specific configuration can be defined using @LoadBalancerClient, while @LoadBalancerClients can provide a default configuration and service-specific overrides.

Q6. Can we implement a custom load-balancing algorithm?

Yes. A custom implementation can use ReactorServiceInstanceLoadBalancer to define how service instances are selected.

Q7. Does Eureka perform load balancing?

Eureka provides service-instance information. Spring Cloud LoadBalancer can use that information to select an instance.

Q8. Can FeignClient use client-side load balancing?

Yes. With the appropriate Spring Cloud dependencies and service-discovery configuration, FeignClient can use Spring Cloud LoadBalancer.

Conclusion

Client-side load balancing is an important component of a scalable microservices architecture. It allows a calling service to select among multiple service instances without hardcoding individual network addresses.

Spring Cloud LoadBalancer integrates with Eureka and clients such as RestTemplate and FeignClient to simplify this process.

By understanding Round Robin, Random Load Balancing, service-specific configurations, and custom strategies, developers can build flexible Spring Boot microservices that distribute requests across available instances.

Leave a Reply