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.

What You Will Need

  • About 15 minutes

  • A favorite text editor or IDE

  • 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

Create a Root Project

This guide walks through building two projects, one of which is a dependency to the other. Consequently, you need to create two child projects under a root project. First, create the build configuration at the top level. For Maven, you need a pom.xml with <modules> that list the subdirectories:

<?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>

For Gradle, you need want a settings.gradle that includes the same directories:

rootProject.name = 'gs-spring-cloud-loadbalancer'

include 'say-hello'
include 'user'

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.

Starting with Spring Initializr

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.2.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.2.0'
	id 'io.spring.dependency-management' version '1.1.4'
	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.2.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>2023.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.2.0'
	id 'io.spring.dependency-management' version '1.1.4'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
	maven { url 'https://repo.spring.io/milestone' }
}

ext {
	set('springCloudVersion', "2023.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
}

Manual Initialization (optional)

If you want to initialize the project manually rather than use the links shown earlier, follow the steps given below:

  1. 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.

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

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

  4. Click Generate.

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

If your IDE has the Spring Initializr integration, you can complete this process from your IDE.

Implement the "Say Hello" service

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:

package hello;

import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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:

  1. Create a src/main/resources directory.

  2. Create a application.yml file within the directory.

  3. 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:

spring:
  application:
    name: say-hello

server:
  port: 8090

Access from a Client Service

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

package hello;

import reactor.core.publisher.Mono;

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:

package hello;

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.

Even though we set up the load-balanced WebClient instance slightly differently for the two endpoints, the end behavior for both is exactly the same. Spring Cloud LoadBalancer is used to select an appropriate instance of the Say Hello service.

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

spring:
  application:
    name: user

server:
  port: 8888

Loadbalance Across Server Instances

Now we can access /hi or hello on the User service and see a friendly greeting:

$ curl http://localhost:8888/hi
Greetings, Mary!

$ curl http://localhost:8888/hi?name=Orontes
Salutations, Orontes!

In WebClientConfig.java, we pass a custom configuration for the LoadBalancer by using the @LoadBalancerClient annotation:

@LoadBalancerClient(name = "say-hello", configuration = SayHelloConfiguration.class)

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:

package hello;

import java.util.Arrays;
import java.util.List;

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)));
  }
}

In that class, we provide a custom ServiceInstanceListSupplier with three hard-coded instances that Spring Cloud LoadBalancer chooses from while making the calls to the Say Hello service.

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:

spring:
  application:
    name: user

server:
  port: 8888

Testing the Loadbalancer

The following listing shows how to run the Say Hello service with Gradle:

$ ./gradlew bootRun

The following listing shows how to run the Say Hello service with Maven:

$ mvn spring-boot:run

To achieve load balancing, you need two servers running separate instances of the same application. You can achieve that by running a second instance of the Say Hello service on a different port. We use port 9999 for this example.

To do so with Gradle, open a new terminal and run the following command:

export SERVER_PORT=9092
./gradlew bootRun

To do so with Maven, open a new terminal and run the following commands:

export SERVER_PORT=9999
mvn spring-boot:run

Then you can start the User service. At this point, you should have three terminals: two for two instances of Say Hello and one for User. Then you can 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:

2016-03-09 21:15:28.915  INFO 90046 --- [nio-8090-exec-7] hello.SayHelloApplication                : Access /greeting

Summary

Congratulations! You have just developed a Spring Loadbalancer application!

Want to write a new guide or contribute to an existing one? Check out our contribution guidelines.

All guides are released with an ASLv2 license for the code, and an Attribution, NoDerivatives creative commons license for the writing.