Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 67

Client-Side Service Discovery with Spring Cloud LoadBalancer

Recommended resources

Hello! Welcome to the next lesson in our Microservices: Resilience & Observability module.

In our last session, we mastered how to make our services more resilient to transient failures using retry mechanisms and exponential backoff. We configured our client to retry calls to a known service address. However, in a modern microservices architecture, especially in cloud environments, service instances are ephemeral—they can be created, destroyed, or moved at any time. Hardcoding IP addresses and ports is therefore a fragile and unscalable approach.

Today, we will solve this fundamental problem. Your learning outcome is to implement client-side service discovery and load balancing using Spring Cloud. You will learn how a client service can dynamically discover the network locations of other services and distribute requests among them. This is a cornerstone pattern of microservice architectures and a frequent topic in senior engineering interviews.

1. The "Why": Understanding Service Discovery

In a monolithic application, components invoke each other through simple method calls. In a distributed system, they communicate over the network. The core problem is: how does Service A know the IP address and port for Service B, especially when there might be multiple instances of Service B running, and these instances can change?

The solution is Service Discovery. The central piece of this pattern is a Service Registry.

  • Service Registry: Think of it as a phone book for your microservices. It's a database containing the network locations (IP and port) of all available service instances.
  • Service Registration: When a new service instance starts up, it registers itself with the service registry, announcing its location and availability.
  • Service Discovery: When one service wants to call another, it queries the service registry to get a list of available, healthy instances and their locations.

To get a clear conceptual overview, let's start with a short video.

Master Service Discovery in Microservices | Eureka and Java Spring Boot

The video 'Master Service Discovery in Microservices' by ByteMonk provides an excellent high-level explanation of the problem and the two primary discovery patterns.

Please watch these two short segments: Introduction to Service Discovery (00:00 - 02:01): Focus on why this pattern is necessary in a microservices architecture and the role of the service registry. Client-Side vs. Server-Side Discovery (02:01 - 03:44): Understand the difference between these two approaches. We will be implementing the client-side pattern today, where the client is responsible for querying the registry and choosing an instance.

As the video explains, in client-side discovery, the client takes on the responsibility of discovering service locations and balancing the load across them. Spring Cloud provides excellent support for this pattern. Our implementation will use Netflix Eureka as the service registry and Spring Cloud LoadBalancer for the client-side logic.


2. Building the Infrastructure

Let's set up the two main components: the Eureka Server (the registry) and a service provider that will register with it.

Step 1: The Eureka Service Registry

The Eureka Server is a standalone Spring Boot application whose only job is to act as our service registry.

Spring Tips: Spring Cloud Loadbalancer #SpringFramework #WithMe

The 'Spring Tips: Spring Cloud Loadbalancer' video by the official SpringDeveloper channel starts by quickly setting up a Eureka server. This is a practical, code-first demonstration.

Watch the segment from 00:22 to 02:45. It covers: The Spring Initializr dependencies for a Eureka Server (Eureka Server). Applying the @EnableEurekaServer annotation to the main application class. The essential application.properties configuration to make it run as a standalone registry.

To summarize the key steps shown in the video:

  1. Dependencies: In your pom.xml, add spring-cloud-starter-netflix-eureka-server.
  2. Enable the Server: Add @EnableEurekaServer to your main Spring Boot application class.
    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaServerApplication {
        public static void main(String[] args) {
            SpringApplication.run(EurekaServerApplication.class, args);
        }
    }
    
  3. Configuration: In application.properties (or .yml), configure the server.
    # Run the server on the standard Eureka port
    server.port=8761
    
    # These two properties tell this instance not to behave like a client.
    # It's a server, so it shouldn't try to register itself.
    eureka.client.register-with-eureka=false
    eureka.client.fetch-registry=false
    

Once you run this application, you can navigate to http://localhost:8761 to see the Eureka dashboard. Initially, it will be empty.

Step 2: The Service Provider (API Service)

Next, we need a microservice that will register itself with Eureka. Let's call it api-service.

Spring Tips: Spring Cloud Loadbalancer #SpringFramework #WithMe

Continuing with the 'Spring Tips' video, the next section shows how to create a simple service and configure it as a Eureka client.

Watch from 02:45 to 05:00. Focus on: The dependencies for a Eureka Client (Eureka Discovery Client). The crucial application.properties settings: spring.application.name and the Eureka server URL.

Key takeaways for creating a Eureka client:

  1. Dependencies: Add spring-cloud-starter-netflix-eureka-client.
  2. Configuration: This is where the magic happens. In application.properties:
    # This logical name is the Service ID. It's how other services will find this one.
    spring.application.name=api-service
    
    # The port this service will run on
    server.port=8081
    
    # The location of the service registry
    eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
    
    With spring-cloud-starter-netflix-eureka-client on the classpath, Spring Boot's autoconfiguration handles the registration automatically. The @EnableEurekaClient annotation is no longer required.

Now, if you run the Eureka Server first, and then this api-service, you should see API-SERVICE appear on the Eureka dashboard after about 30 seconds.


3. Implementing the Client with Load Balancing

Now for the main event: creating a client-service that can find and call our api-service without knowing its host:port. We will use Spring Cloud LoadBalancer, the modern, non-blocking load balancer in the Spring ecosystem that has replaced Netflix Ribbon.

Our goal is to make an HTTP call to a logical URL like http://api-service/greetings and have Spring Cloud automatically resolve api-service to a healthy instance (e.g., http://localhost:8081).

The most common and idiomatic way to achieve this is by using a @LoadBalanced WebClient.Builder.

Spring Tips: Spring Cloud Loadbalancer #SpringFramework #WithMe

The 'Spring Tips' video provides a clear, three-stage demonstration of using Spring Cloud LoadBalancer. We'll focus on the final, most practical approach.

Watch the segment from 15:49 to 18:00. This demonstrates the @LoadBalanced annotation approach, which is the most common and what you'd likely use in a real project. Pay attention to: How a WebClient.Builder bean is annotated with @LoadBalanced. The property to disable the legacy Ribbon load balancer. How the WebClient call is made using the service name (api) in the URL.

Here's the implementation breakdown for your client-service:

  1. Dependencies: Add spring-cloud-starter-netflix-eureka-client (to find the registry) and spring-cloud-starter-loadbalancer (for the load balancing logic).

  2. Configuration (application.yml):

    spring:
      application:
        name: client-service
      # This is crucial! It tells Spring Cloud to use the new LoadBalancer, not the old Ribbon.
      cloud:
        loadbalancer:
          ribbon:
            enabled: false
    
    server:
      port: 8082
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka/
    
  3. Create the @LoadBalanced WebClient Bean:
    In a @Configuration class, define a bean for the WebClient.Builder and annotate it with @LoadBalanced. This annotation instructs Spring Cloud to add a special filter that understands how to resolve service names.

    import org.springframework.cloud.client.loadbalancer.LoadBalanced;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.reactive.function.client.WebClient;
    
    @Configuration
    public class WebClientConfig {
    
        @Bean
        @LoadBalanced // The magic happens here!
        public WebClient.Builder loadBalancedWebClientBuilder() {
            return WebClient.builder();
        }
    }
    
  4. Use the WebClient to Call the Service:
    Now you can inject and use WebClient (or its builder) to make calls using the logical service ID.

    @Service
    public class ApiClient {
    
        private final WebClient webClient;
    
        // Spring injects a WebClient instance built from our @LoadBalanced builder
        public ApiClient(WebClient.Builder webClientBuilder) {
            this.webClient = webClientBuilder.build();
        }
    
        public Mono<String> callApiService() {
            // Note the URL: we use the service ID "api-service", not a hostname.
            return this.webClient.get()
                    .uri("http://api-service/greetings")
                    .retrieve()
                    .bodyToMono(String.class);
        }
    }
    

When callApiService() is executed, the @LoadBalanced mechanism intercepts the request. It queries its local copy of the Eureka registry for all instances of api-service, chooses one (by default, using a round-robin algorithm), and transparently rewrites the URL to the actual host:port before sending the request.

This diagram helps visualize the flow. While it shows RestTemplate, the principle is identical for WebClient: the LoadBalancerInterceptor (or in our case, ReactorLoadBalancerExchangeFilterFunction) works with the LoadBalancerClient to resolve the service ID to a concrete instance.

Spring Cloud LoadBalancer Internal Flow
This diagram illustrates the internal components of Spring Cloud's client-side load balancing. A request made with a service ID is intercepted, a `LoadBalancer` implementation (like Round-Robin) chooses a `ServiceInstance` from a list provided by the `DiscoveryClient`, and the request is forwarded to the resolved physical address.
Test your understanding!

What would happen if you used a regular, non-load-balanced WebClient instance to make a call to http://api-service/greetings? What kind of error would you expect?

Show answer

You would get an UnknownHostException (or a similar DNS resolution error). The operating system's DNS resolver has no idea what api-service means; it's a logical name understood only by the Spring Cloud service discovery mechanism. The @LoadBalanced annotation is what enables WebClient to translate this logical name into a physical IP address by consulting the service registry.


4. Interview Focus: Explaining the End-to-End Flow

A common senior-level interview question is to explain this entire process. Your ability to articulate the interaction between components demonstrates a deep understanding of distributed systems.

Question: "You have a ShoppingCartService that needs to call a ProductService. How would you implement resilient communication using client-side service discovery in Spring Cloud?"

A Strong Answer:

  1. Service Registry: "First, I'd set up a service registry, like Netflix Eureka, using @EnableEurekaServer. This acts as a central directory for all my services."

  2. Service Registration: "The ProductService would be configured as a Eureka client. By including the eureka-client dependency and setting its spring.application.name to product-service, it will automatically register its IP and port with the Eureka server on startup."

  3. Client-Side Discovery & Load Balancing: "In the ShoppingCartService, I would include the eureka-client and spring-cloud-loadbalancer starters. I'd then define a WebClient.Builder bean and annotate it with @LoadBalanced."

  4. Making the Call: "When calling the ProductService, I would use the configured WebClient to make a request to the logical URL, http://product-service/products/{id}. I would also disable the legacy Ribbon load balancer via spring.cloud.loadbalancer.ribbon.enabled=false to ensure I'm using the modern, reactive stack."

  5. The "Under the Hood" Mechanics: "When the call is made, the @LoadBalanced filter intercepts it. It uses the DiscoveryClient to get a list of all healthy product-service instances from its local cache of the Eureka registry. The Spring Cloud LoadBalancer then applies a load-balancing strategy—by default, round-robin—to select one instance. Finally, it rewrites the URL to that instance's physical address (e.g., http://10.0.1.23:8080/products/{id}) and sends the request. This makes the client resilient to individual instance failures and allows the ProductService to be scaled out transparently."

You can also showcase further knowledge by mentioning advanced features from the Spring Cloud LoadBalancer documentation (LINK), such as instance caching, health checks, or pluggable load-balancing algorithms (e.g., Random vs. Round-Robin).


Conclusion

Congratulations! You have just implemented one of the most critical patterns in a microservices architecture. You've moved from brittle, hardcoded communication to a dynamic and resilient system where services can discover and adapt to each other.

Key Takeaways:

  • Service Discovery is essential in dynamic environments to locate service instances.
  • Netflix Eureka is a popular choice for a Service Registry in the Spring ecosystem.
  • Spring Cloud LoadBalancer provides modern, reactive client-side load balancing.
  • The @LoadBalanced annotation on a WebClient.Builder is the key to enabling this behavior, allowing you to call services by their logical spring.application.name.
  • Always remember to disable Ribbon (spring.cloud.loadbalancer.ribbon.enabled=false) to use the current-generation load balancer.

Preview of the Next Lesson:
Our services can now find each other, but each one still has its own application.properties or application.yml file. What happens when you have dozens of services and need to change a shared property, like a database URL or a feature flag? Managing this manually is error-prone and slow. In our next lesson, we will tackle this by learning to implement centralized configuration for microservices using Spring Cloud Config.