Client-Side Load-Balancing with Spring Cloud LoadBalancer

This guide walks you through the process of creating load-balanced microservices.

What You Will Build

You will build a microservice application that uses Spring Cloud LoadBalancer to provide client-side load-balancing in calls to another microservice.

  • JDK 1.8 or later

  • Gradle 6+ or Maven 3.5+

  • You can also import the code straight into your IDE:

  • Spring Tool Suite (STS) or IntelliJ IDEA

  • <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>org.springframework</groupId>
        <artifactId>gs-spring-cloud-loadbalancer</artifactId>
        <version>0.1.0</version>
        <packaging>pom</packaging>
        <modules>
          <module>say-hello</module>
          <module>user</module>
        </modules>
    </project>

    Optionally, you can include an empty build.gradle (to help IDEs identify the root directory).

    Create the Directory Structure

    In the directory that you want to be your root directory, create the following subdirectory structure (for example, with mkdir say-hello user on *nix systems):

    └── say-hello
    └── user

    In the root of the project, you need to set up a build system, and this guide shows you how to use Maven or Gradle.

    If you use Maven for the Say Hello project, visit the Spring Initializr to generate a new project with the required dependency (Spring Web).

    The following listing shows the pom.xml file that is created when you choose Maven:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>3.0.0</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>com.example</groupId>
    	<artifactId>spring-cloud-loadbalancer-say-hello</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>spring-cloud-loadbalancer-say-hello</name>
    	<description>Demo project for Spring Boot</description>
    	<properties>
    		<spring-boot.repackage.skip>true</spring-boot.repackage.skip>
    		<java.version>17</java.version>
    	</properties>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    </project>

    If you use Gradle for the Say Hello project, visit the Spring Initializr to generate a new project with the required dependency (Spring Web).

    The following listing shows the build.gradle file that is created when you choose Gradle:

    plugins {
    	id 'org.springframework.boot' version '3.0.0'
    	id 'io.spring.dependency-management' version '1.1.0'
    	id 'java'
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '17'
    repositories {
    	mavenCentral()
    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-web'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    test {
    	useJUnitPlatform()
    bootJar {
    	enabled = false
       

    If you use Maven for the User project, visit the Spring Initializr to generate a new project with the required dependencies (Cloud Loadbalancer and Spring Reactive Web).

    The following listing shows the pom.xml file that is created when you choose Maven:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>3.0.0</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>com.example</groupId>
    	<artifactId>spring-cloud-loadbalancer-user</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>spring-cloud-loadbalancer-user</name>
    	<description>Demo project for Spring Boot</description>
    	<properties>
    		<spring-boot.repackage.skip>true</spring-boot.repackage.skip>
    		<java.version>17</java.version>
    		<spring-cloud.version>2022.0.0</spring-cloud.version>
    	</properties>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-webflux</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.cloud</groupId>
    			<artifactId>spring-cloud-starter-loadbalancer</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    		<dependency>
    			<groupId>io.projectreactor</groupId>
    			<artifactId>reactor-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    	<dependencyManagement>
    		<dependencies>
    			<dependency>
    				<groupId>org.springframework.cloud</groupId>
    				<artifactId>spring-cloud-dependencies</artifactId>
    				<version>${spring-cloud.version}</version>
    				<type>pom</type>
    				<scope>import</scope>
    			</dependency>
    		</dependencies>
    	</dependencyManagement>
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    	<repositories>
    		<repository>
    			<id>spring-milestones</id>
    			<name>Spring Milestones</name>
    			<url>https://repo.spring.io/milestone</url>
    		</repository>
    	</repositories>
    </project>

    If you use Gradle for the User project, visit the Spring Initializr to generate a new project with the required dependencies (Cloud Loadbalancer and Spring Reactive Web).

    The following listing shows the build.gradle file that is created when you choose Gradle:

    plugins {
    	id 'org.springframework.boot' version '3.0.0'
    	id 'io.spring.dependency-management' version '1.1.0'
    	id 'java'
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '17'
    repositories {
    	mavenCentral()
    	maven { url 'https://repo.spring.io/milestone' }
    ext {
    	set('springCloudVersion', "2022.0.0")
    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-webflux'
    	implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    	testImplementation 'io.projectreactor:reactor-test'
    dependencyManagement {
    	imports {
    		mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    test {
    	useJUnitPlatform()
    bootJar {
    	enabled = false
         
  • Navigate to https://start.spring.io. This service pulls in all the dependencies you need for an application and does most of the setup for you.

  • Choose either Gradle or Maven and the language you want to use. This guide assumes that you chose Java.

  • Click Dependencies and select Spring Web (for the Say Hello project) or Cloud Loadbalancer and Spring Reactive Web (for the User project).

  • Click Generate.

  • Download the resulting ZIP file, which is an archive of a web application that is configured with your choices.

  • Our “server” service is called Say Hello. It returns a random greeting (picked out of a static list of three) from an endpoint that is accessible at /greeting.

    In src/main/java/hello, create the file SayHelloApplication.java.

    The following listing shows the contents of say-hello/src/main/java/hello/SayHelloApplication.java:

    import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class SayHelloApplication { private static Logger log = LoggerFactory.getLogger(SayHelloApplication.class); public static void main(String[] args) { SpringApplication.run(SayHelloApplication.class, args); @GetMapping("/greeting") public String greet() { log.info("Access /greeting"); List<String> greetings = Arrays.asList("Hi there", "Greetings", "Salutations"); Random rand = new Random(); int randomNum = rand.nextInt(greetings.size()); return greetings.get(randomNum); @GetMapping("/") public String home() { log.info("Access /"); return "Hi!";

    It is a simple @RestController, where we have one @RequestMapping method for the /greeting and another for the root path /.

    We are going to run multiple instances of this application locally alongside a client service application. To get started:

  • Create a src/main/resources directory.

  • Create a application.yml file within the directory.

  • In that file, set a default value for server.port.

  • (We will instruct the other instances of the application to run on other ports so that none of the Say Hello instances conflict with the client when we get that running). While we are in this file, we can set the spring.application.name for our service too.

    The following listing shows the contents of say-hello/src/main/resources/application.yml:

    Our users see the User application. It makes a call to the Say Hello application to get a greeting and then sends that greeting to our user when the user visits the endpoints at /hi and /hello.

    In the User application directory, under src/main/java/hello, add the UserApplication.java file:

    The following listing shows the contents of user/src/main/java/hello/UserApplication.java

    import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.function.client.WebClient; * @author Olga Maciaszek-Sharma @SpringBootApplication @RestController public class UserApplication { private final WebClient.Builder loadBalancedWebClientBuilder; private final ReactorLoadBalancerExchangeFilterFunction lbFunction; public UserApplication(WebClient.Builder webClientBuilder, ReactorLoadBalancerExchangeFilterFunction lbFunction) { this.loadBalancedWebClientBuilder = webClientBuilder; this.lbFunction = lbFunction; public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); @RequestMapping("/hi") public Mono<String> hi(@RequestParam(value = "name", defaultValue = "Mary") String name) { return loadBalancedWebClientBuilder.build().get().uri("http://say-hello/greeting") .retrieve().bodyToMono(String.class) .map(greeting -> String.format("%s, %s!", greeting, name)); @RequestMapping("/hello") public Mono<String> hello(@RequestParam(value = "name", defaultValue = "John") String name) { return WebClient.builder() .filter(lbFunction) .build().get().uri("http://say-hello/greeting") .retrieve().bodyToMono(String.class) .map(greeting -> String.format("%s, %s!", greeting, name));

    We also need a @Configuration class where we set up a load-balanced WebClient.Builder instance:

    The following listing shows the contents of user/src/main/java/hello/WebClientConfig.java:

    import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; @Configuration @LoadBalancerClient(name = "say-hello", configuration = SayHelloConfiguration.class) public class WebClientConfig { @LoadBalanced @Bean WebClient.Builder webClientBuilder() { return WebClient.builder();

    The configuration provides a @LoadBalanced WebClient.Builder instance, which we use when someone hits the hi endpoint of UserApplication.java. Once the hi endpoint is hit, we use this builder to create a WebClient instance, which makes an HTTP GET request to the Say Hello service’s URL and gives us the result as a String.

    In UserApplication.java, we have also added a /hello endpoint that does the same action. However, rather than use the @LoadBalanced annotation, we use an @Autowired load-balancer exchange filter function (lbFunction), which we pass by using the filter() method to a WebClient instance that we programmatically build.

    Add the spring.application.name and server.port properties to src/main/resources/application.properties or src/main/resources/application.yml:

    The following listing shows the contents of user/src/main/resources/application.yml

    This means that, whenever a service named say-hello is contacted, instead of running with the default setup, Spring Cloud LoadBalancer uses the configuration provided in SayHelloConfiguration.java.

    The following listing shows the contents of user/src/main/java/hello/SayHelloConfiguration.java:

    import reactor.core.publisher.Flux; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; * @author Olga Maciaszek-Sharma public class SayHelloConfiguration { @Bean @Primary ServiceInstanceListSupplier serviceInstanceListSupplier() { return new DemoServiceInstanceListSuppler("say-hello"); class DemoServiceInstanceListSuppler implements ServiceInstanceListSupplier { private final String serviceId; DemoServiceInstanceListSuppler(String serviceId) { this.serviceId = serviceId; @Override public String getServiceId() { return serviceId; @Override public Flux<List<ServiceInstance>> get() { return Flux.just(Arrays .asList(new DefaultServiceInstance(serviceId + "1", serviceId, "localhost", 8090, false), new DefaultServiceInstance(serviceId + "2", serviceId, "localhost", 9092, false), new DefaultServiceInstance(serviceId + "3", serviceId, "localhost", 9999, false))); This step has been added to explain how you can pass your own custom configuration to the Spring Cloud LoadBalancer. However, you need not use the @LoadBalancerClient annotation and create your own configuration for the LoadBalancer. The most typical way is to use Spring Cloud LoadBalancer with service discovery. If you have any DiscoveryClient on your classpath, the default Spring Cloud LoadBalancer configuration uses it to check for service instances. As a result, you only choose from instances that are up and running. You can learn how to use ServiceDiscovery with this guide.

    We also add an application.yml file with default server.port and spring.application.name.

    The following listing shows the contents of user/src/main/resources/application.yml:

    Then you can start the User service. To do so, access localhost:8888/hi and watch the Say Hello service instances.

    Your requests to the User service should result in calls to Say Hello being spread across the running instances in round-robin fashion: