Go back to Blogs
Service Discovery Magic in a Spring Microservices
ℹ️
- We sometimes use affiliate links in our content. This means that if you click on a link and make a purchase, we may receive a small commission at no extra cost to you. This helps us keep creating valuable content for you!
Prerequisites
Context
In the world of microservices and distributed systems, applications are decomposed into smaller, independent services that communicate with each other. This architecture offers numerous benefits, including enhanced scalability, increased flexibility, and improved fault isolation. However, it also introduces new challenges, with one of the most significant being service discovery.
Problem
How do these independent services find and communicate with each other when their IP addresses and ports can change dynamically? This is where service discovery becomes essential. Without a robust service discovery mechanism, managing the dynamic nature of microservices can become complex and error-prone, leading to potential communication failures and increased operational overhead.

Solution
In a microservices architecture deployed on cloud (for instance in AWS), service discovery is crucial for enabling independent services to find and communicate with each other despite dynamic changes in their IP addresses and ports. Spring Cloud provides robust support for service discovery through tools like Eureka. By integrating Spring Cloud Eureka into your Spring Boot applications, you can register each microservice with a Eureka server, which maintains a registry of available services. This allows services to dynamically discover and communicate with each other using logical service names instead of hard-coded IP addresses. This approach simplifies the development, deployment, and management of microservices, ensuring scalability, flexibility, and fault tolerance.

Let’s delve into the concept of service discovery in Spring Boot, exploring its importance, different approaches, and practical implementation using Spring Cloud Eureka Server.
Why Service Discovery is Crucial in Microservices?
Imagine a monolithic application where all components reside in a single codebase and communicate directly. Deploying and scaling such an application can be complex. Microservices address this by decoupling the application into smaller, manageable units. However, this decoupling creates a new challenge: managing service locations.
- Dynamic Environments: Microservices are often deployed in dynamic environments like cloud platforms or containerized environments. IP addresses and ports of services can change frequently due to scaling, failures, or deployments. Hardcoding these values is impractical and unreliable.
- Load Balancing: Distributing traffic across multiple instances of a service is essential for high availability and performance. Service discovery mechanisms can integrate with load balancers to route requests efficiently.
- Centralized Management: Service discovery tools provide a centralized registry of all available services and their locations. This simplifies service management and monitoring.
- Decoupling: Service consumers don’t need to know the specific location of a service instance. They can rely on the service registry to discover the appropriate endpoint.
Approaches to Service Discovery
There are two main approaches to service discovery:
- Client-Side Discovery: In this approach, the client is responsible for discovering the service instances. The client typically queries a service registry to obtain the list of available instances and then uses a load balancing algorithm to choose one.
- Server-Side Discovery: With server-side discovery, a dedicated component, such as a load balancer or API gateway, handles the service discovery process. The client sends requests to this component, which then forwards the request to an appropriate service instance.
Spring Boot supports both client-side and server-side discovery, offering flexibility based on your needs.
Popular Service Discovery Tools
Several excellent tools are available for implementing service discovery:
- Netflix Eureka: A widely used service registry, particularly within the Spring Cloud ecosystem. It’s relatively simple to set up and configure. Eureka consists of a server (the registry) and clients (the services registering themselves).
- HashiCorp Consul: A powerful and feature-rich service discovery and configuration management tool. Consul offers features like health checking, key-value storage, and DNS-based service discovery.
- Apache ZooKeeper: A distributed coordination service that can also be used for service discovery. ZooKeeper is known for its reliability and robustness.
Implementing Service Discovery with Spring Boot and Spring Cloud
Let’s illustrate how to implement service discovery using Spring Boot, focusing specifically on Eureka with a practical example involving two microservices: a Product Service and an Order Service. We’ll walk through setting up the Eureka Server (registry) and registering both services, demonstrating how the Order Service can discover and consume the Product Service.
Eureka Server (Registry)
The Eureka Server acts as a service registry where all microservices register themselves and discover other services. To set up a Eureka Server, you need to add the spring-cloud-starter-netflix-eureka-server dependency in your pom.xml or build.gradle file. The main application class should be annotated with @EnableEurekaServer to enable the Eureka Server functionality. The application.properties or application.yml file should include configuration properties such as the server port and Eureka-specific settings.
- Add Eureka Server dependency in the project dependencies in pom.xml file:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
- Create Eureka Server application class (EurekaServerApplication.java) with @SpringBootApplication and @EnableEurekaSever annotations. The @SpringBootApplication annotation enables Spring Boot Configuration, auto configuration and Component scanning, while @EnableEurekaServer is a spring cloud annotation that enables eureka service registry functionalities.
package com.ngntechware.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
- Add application properties in application.yml file: By default, the registry also tries to register itself, so we need to disable that behavior. Additionally, it is a good convention to put this registry on a separate port when using it locally. In a production environment, we may have more than one instance of the registry.
spring:
application:
name: eureka-server
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
logging:
level:
com.netflix.eureka: OFF
com.netflix.discovery: OFF
Product Service
The Product Service is a microservice that registers itself with the Eureka Server. It includes the spring-cloud-starter-netflix-eureka-client dependency. The application.properties or application.yml file should contain Eureka client configurations, such as the Eureka server URL. The service includes a Product REST controller to handle product-related requests, such as to add a product into a database and to retrieve a product or products from the database
- Add Dependencies to handle REST call and to enable registration of the product service in the Eureka Server (Registry)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
- Create the main application class (ProductMicroServiceApplication.java) to manage Spring Boot properties with @SpringBootApplication annotation.
package com.ngntechware.microservicea;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ProductMicroserviceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductMicroserviceApplication.class, args);
}
}
- Create a POJO class (ProductDTO.class) to define the product properties. In our case we added the product name and description.
package com.ngntechware.microservicea;
public class ProductDTO {
private String name;
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
- Create a Product Controller Class (ProductRestController.java) to manage REST API calls associated with Product Service. We added get products method to handle API Call to retrieve all the available products.
package com.ngntechware.microservicea;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class ProductRestController {
private final List<ProductDTO> products = List.of(
getProduct("BMW-E13", "E Model (1965 - 2013)"),
getProduct("BMW-F32", "F Model (2009 - 2021)"),
getProduct("BMW-G11", "G Model (2016 - )"),
getProduct("BMW-X5", "Sporty SUVs & Crossovers")
);
@GetMapping(path = "/products", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<ProductDTO>> getProducts() {
return ResponseEntity.ok(products);
}
private ProductDTO getProduct(String name, String description) {
ProductDTO product = new ProductDTO();
product.setName(name);
product.setDescription(description);
return product;
}
}
- Set some Application Properties, such as application name and server port in application.yml
spring:
application:
name: product-microservice
server:
port: 8081
Order Service
Similar to the Product Service, the Order Service registers itself with the Eureka Server using the spring-cloud-starter-netflix-eureka-client dependency. The application.properties or application.yml file should include the necessary Eureka client configurations. The Order Service typically includes REST controllers to handle order-related requests.
- Add Dependencies to handle REST call and to enable registration of the order service in the Eureka Server (Registry)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependency>
</dependencies>
- Create the main application class (OrderMicroServiceApplication.java) to manage Spring Boot properties with @SpringBootApplication annotation. The Spring Cloud @LoadBalanced annotation on the RestClient enables load balancing capabilities, automatically resolving service names to actual product microservice instances.
package com.ngntechware.microserviceb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
@SpringBootApplication
public class OrderMicroserviceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderMicroserviceApplication.class, args);
}
@LoadBalanced
@Bean
public RestClient restClient() {
return RestClient.builder().build();
}
}
- Create Order REST Controller (OrderRestController Class) to expose the endpoint that calls the product microservice. The class uses a DiscoveryClient to find the serviceId of the product-microservice based only on the application name. In this blog example, we have only one instance of product-microservice, hence we look only at the first instance. Once we have the instance that references the location of the product-microservice, we can use the information in Rest Client call.
package com.ngntechware.microserviceb;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
@RestController
public class OrderRestController {
private final DiscoveryClient discoveryClient;
private final RestClient restClient;
public OrderRestController(DiscoveryClient discoveryClient, RestClient restClient) {
this.discoveryClient = discoveryClient;
this.restClient = restClient;
}
@GetMapping(path = "/orders", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> getOrders() {
ServiceInstance serviceInstance = discoveryClient.getInstances("product-microservice").get(0);
return ResponseEntity.ok(restClient.get()
.uri(serviceInstance.getUri() + "/products")
.retrieve()
.body(String.class));
}
}
- Set some application Properties like the application name and server port in application.yml just like the product microservice.
spring:
application:
name: order-microservice
server:
port: 8082
Test the Applications
- Start Eureka-Server: Start Eureka-Server with the command ./mvnw spring-boot:run command – the EurekaServerApplication will start on local port 8761
- Start Product Microservice: Start the Product Microservice with the command ./mwnw spring-boot:run – the product-microservice will start on local port 8081 and will be registered on the Eureka server as shown in the terminal log

- Start the Order Orderservice: Start the Order Microservice with the command ./mwnw spring-boot:run – the product-microservice will start on local port 8082 and will be registered on the Eureka server as shown in the terminal log
We can observe the product-microservice and order-microservice registration on the Eureka Dashboard at http://localhost:8761/

- Run the command curl http://localhost:8082/orders test all three applications are working properly – it retrieves all the available orders.
[
{
"name": "BMW-E13",
"description": "E Model (1965 - 2013)"
},
{
"name": "BMW-F32",
"description": "F Model (2009 - 2021)"
},
{
"name": "BMW-G11",
"description": "G Model (2016 - )"
},
{
"name": "BMW-X5",
"description": "Sporty SUVs & Crossovers"
}
]
The example provides a basic but functional interaction between two microservices, demonstrating the core concepts of service discovery with Eureka in a Spring Boot microservices environment. You can further extend this example by adding more services, implementing more complex logic, and exploring other features of Eureka like health checks and instance metadata.
If our applications are running in the cloud, we can deploy multiple instances of the product-microservice in different availability zones. By leveraging Spring Cloud, we can automatically handle service registry and load balancing, ensuring high availability and fault tolerance across the distributed instances.
Conclusion
Service discovery is a fundamental aspect of microservices architecture, enabling services to dynamically discover and communicate with each other. This simplifies development, deployment, and management. Spring Boot offers excellent support for integrating with various service discovery tools, empowering you to build resilient and scalable microservices applications. This blog post has provided a comprehensive overview of service discovery in Spring Boot, covering its importance, different approaches, and practical implementation using Spring Cloud Eureka. By understanding these concepts, you can effectively leverage service discovery to build robust and maintainable microservices architectures. For more in-depth information and advanced configuration options, be sure to explore the Spring boot cloud documentations provided in the references for your chosen service discovery tool.