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

What you’ll build

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

What you’ll need

How to complete this guide

Like most Spring Getting Started guides, you can start from scratch and complete each step or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.

To start from scratch, move on to Build with Gradle.

To skip the basics, do the following:

When you finish, you can check your results against the code in draft-gs-template/complete.

Build with Gradle

Build with Gradle

First you set up a basic build script. You can use any build system you like when building apps with Spring, but the code you need to work with Gradle and Maven is included here. If you’re not familiar with either, refer to Building Java Projects with Gradle or Building Java Projects with Maven.

Create the directory structure

In a project directory of your choosing, create the following subdirectory structure; for example, with mkdir -p src/main/java/hello on *nix systems:

└── src
    └── main
        └── java
            └── hello

Create a Gradle build file

build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.2.1.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    baseName = 'draft-gs-template'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

The Spring Boot gradle plugin provides many convenient features:

  • It collects all the jars on the classpath and builds a single, runnable "über-jar", which makes it more convenient to execute and transport your service.

  • It searches for the public static void main() method to flag as a runnable class.

  • It provides a built-in dependency resolver that sets the version number to match Spring Boot dependencies. You can override any version you wish, but it will default to Boot’s chosen set of versions.

Build with Maven

Build with Maven

First you set up a basic build script. You can use any build system you like when building apps with Spring, but the code you need to work with Maven is included here. If you’re not familiar with Maven, refer to Building Java Projects with Maven.

Create the directory structure

In a project directory of your choosing, create the following subdirectory structure; for example, with mkdir -p src/main/java/hello on *nix systems:

└── src
    └── main
        └── java
            └── hello

pom.xml

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>draft-gs-template</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>

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

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

The Spring Boot Maven plugin provides many convenient features:

  • It collects all the jars on the classpath and builds a single, runnable "über-jar", which makes it more convenient to execute and transport your service.

  • It searches for the public static void main() method to flag as a runnable class.

  • It provides a built-in dependency resolver that sets the version number to match Spring Boot dependencies. You can override any version you wish, but it will default to Boot’s chosen set of versions.

Build with your IDE

Build with your IDE

Implement "Say Hello" service

Our "server" service is called "Say Hello". It will return a random greeting (picked out of a static list of three) from an endpoint accessible at /greeting.

In src/main/java/hello, create the file SayHelloApplication.java. It should look like this:

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’s a simple @RestController, where we have one @RequestMapping method for /greeting and then another for the root path /.

We’re going to run multiple instances of this application locally alongside a client service application, so create the directory src/main/resources, create the file application.yml within it, and then, in that file, set a default value for server.port. (We’ll instruct the other instances of the application to run on other ports, as well, so that none of the Say Hello instances will conflict with the client when we get that running). While we’re in this file, we’ll set the spring.application.name for our service too.

say-hello/src/main/resources/application.yml

spring:
  application:
    name: say-hello

server:
  port: 8090

Access from a client service

The "User" application will be what our user sees. It will make a call to the Say Hello application to get a greeting and then send that 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 file UserApplication.java:

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 add a @Configuration class where we will set up a load-balanced WebClient.Builder instance:

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, that we use when someone hits the hi endpoint of UserApplication.java. Once the hi endpoint is hit, we will use this builder to create a WebClient instance which will be used to make an HTTP GET request to the Say Hello service’s URL and give us the result as a String.

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

Even though we set up the load-balanced WebClient instance slightly differently for the two endpoints, the end behaviour for both is exactly the same. Spring Cloud LoadBalancer is used underneath 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:

user/src/main/resources/application.yml

spring:
  application:
    name: user

server:
  port: 8888

Load-balance 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!

As you will see, in WebClientConfig.java, we pass a custom configuration for the LoadBalancer using the @LoadBalancerClient annotation:

@LoadBalancerClient(name = "say-hello", configuration = SayHelloConfiguration.class). This means that whenever a service named say-hello is being contacted, instead of running with the default setup, Spring Cloud LoadBalancer will use the configuration provided in SayHelloConfiguration.java:

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.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * @author Olga Maciaszek-Sharma
 */
@Configuration
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)));
	}
}

There, we provide a custom ServiceInstanceListSupplier with 3 hard-coded instances that Spring Cloud LoadBalancer will choose 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 don’t need to 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 will use it under the hood to check for service instances. Like this, you will also 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:

user/src/main/resources/application.yml

spring:
  application:
    name: user

server:
  port: 8888

Trying it out

Run the Say Hello service, using either Gradle:

$ ./gradlew bootRun

or Maven:

$ mvn spring-boot:run

Run other instances on ports 9092 and 9999, again using either Gradle:

$ SERVER_PORT=9092 ./gradlew bootRun

or Maven:

$ SERVER_PORT=9999 mvn spring-boot:run

Then start up the User service. Access localhost:8888/hi and then watch the Say Hello service instances.

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

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

Test the application

Now that the application is running, you can test it.

Summary

Congratulations! You’ve just developed a Spring 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.