Skip to main content

Chapter 2: Service Discovery with Eureka

🎯 Chapter Objectives​

  • Understand the concept of Service Discovery
  • Configure an Eureka Server
  • Register services as Eureka Clients
  • Implement client-side Load Balancing
  • Manage high availability of Eureka

1. Why Service Discovery?​

1.1 The Problem Without Service Discovery​

Problems:

  • Hardcoded URLs in configuration
  • If an IP changes β†’ modify all clients
  • If an instance is added β†’ modify all clients
  • Load balancing impossible
  • No failure detection

1.2 The Solution: Service Discovery​

Advantages:

  • Services register dynamically
  • Clients discover services by name
  • Automatic load balancing
  • Automatic failure detection
  • Transparent scaling

2. Netflix Eureka​

2.1 What is Eureka?​

Eureka is a discovery service developed by Netflix, composed of:

  • Eureka Server: The central registry
  • Eureka Client: Library in each service

2.2 Architecture​

2.3 Key Concepts​

ConceptDescription
RegisterService registers at startup
Renew (Heartbeat)Ping every 30 seconds
Fetch RegistryClient retrieves the list of services
CancelService unregisters on shutdown
EvictionEureka removes services without heartbeat (90s)

3. Configuring Eureka Server​

3.1 Create the Project​

Project Structure:

eureka-server/
β”œβ”€β”€ pom.xml
└── src/main/java/
└── com/example/eurekaserver/
└── EurekaServerApplication.java
└── src/main/resources/
└── application.yml

3.2 Maven Dependencies​

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

<groupId>com.example</groupId>
<artifactId>eureka-server</artifactId>
<version>1.0.0</version>
<name>eureka-server</name>

<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>

<dependencies>
<!-- Eureka Server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

<!-- Security (optional but recommended) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</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>
</project>

3.3 Main Application​

package com.example.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer // Enables the Eureka server
public class EurekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

3.4 Configuration application.yml​

server:
port: 8761

spring:
application:
name: eureka-server

eureka:
instance:
hostname: localhost
client:
# Do not register itself (it's the server!)
register-with-eureka: false
# Do not fetch the registry (it's the server!)
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
server:
# Disable self-preservation mode in dev
enable-self-preservation: false
# Dead instance cleanup interval
eviction-interval-timer-in-ms: 5000

# Logging
logging:
level:
com.netflix.eureka: INFO
com.netflix.discovery: INFO

3.5 Securing Eureka (Optional)​

package com.example.eurekaserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(basic -> {});

return http.build();
}

@Bean
public UserDetailsService userDetailsService() {
var user = User.builder()
.username("eureka")
.password(passwordEncoder().encode("eureka123"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

3.6 Eureka Dashboard​

After starting, access: http://localhost:8761

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ EUREKA SERVER β”‚
β”‚ Spring Cloud β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ System Status β”‚
β”‚ Environment: test β”‚
β”‚ Data center: default β”‚
β”‚ Current time: 2024-01-15 10:30:45 β”‚
β”‚ Uptime: 00:15:30 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Instances currently registered with Eureka β”‚
β”‚ β”‚
β”‚ Application AMIs Availability Zones Status β”‚
β”‚ USER-SERVICE n/a (2) (2) UP (2) β”‚
β”‚ PRODUCT-SERVICE n/a (1) (1) UP (1) β”‚
β”‚ ORDER-SERVICE n/a (1) (1) UP (1) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4. Configuring Eureka Client​

4.1 Maven Dependencies (Service)​

<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<!-- Actuator (for health checks) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

4.2 Main Application​

package com.example.userservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// @EnableDiscoveryClient is optional since Spring Cloud 2020
// Discovery is automatic if eureka-client is in the classpath
public class UserServiceApplication {

public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

4.3 Client Configuration​

server:
port: 8081

spring:
application:
name: user-service # Service name in Eureka

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# Registry fetch interval
registry-fetch-interval-seconds: 5
instance:
# Use IP instead of hostname
prefer-ip-address: true
# Unique instance ID
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
# Heartbeat interval
lease-renewal-interval-in-seconds: 10
# Expiration time (3x renewal)
lease-expiration-duration-in-seconds: 30

# Actuator for health checks
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always

4.4 Configuration with Security​

If Eureka Server is secured:

eureka:
client:
service-url:
# Format: http://user:password@host:port/eureka/
defaultZone: http://eureka:eureka123@localhost:8761/eureka/

5. Complete Example: User Service​

5.1 Project Structure​

user-service/
β”œβ”€β”€ pom.xml
└── src/main/java/com/example/userservice/
β”œβ”€β”€ UserServiceApplication.java
β”œβ”€β”€ controller/
β”‚ └── UserController.java
β”œβ”€β”€ model/
β”‚ └── User.java
└── service/
└── UserService.java

5.2 Model​

package com.example.userservice.model;

public class User {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;

// Constructors
public User() {}

public User(Long id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}

// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }

public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}

5.3 Service​

package com.example.userservice.service;

import com.example.userservice.model.User;
import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;
import java.util.*;

@Service
public class UserService {

private final Map<Long, User> users = new HashMap<>();

@PostConstruct
public void init() {
users.put(1L, new User(1L, "john", "john@example.com", "John", "Doe"));
users.put(2L, new User(2L, "jane", "jane@example.com", "Jane", "Smith"));
users.put(3L, new User(3L, "bob", "bob@example.com", "Bob", "Wilson"));
}

public List<User> getAllUsers() {
return new ArrayList<>(users.values());
}

public Optional<User> getUserById(Long id) {
return Optional.ofNullable(users.get(id));
}

public User createUser(User user) {
Long newId = users.keySet().stream()
.max(Long::compareTo)
.orElse(0L) + 1;
user.setId(newId);
users.put(newId, user);
return user;
}

public boolean existsById(Long id) {
return users.containsKey(id);
}
}

5.4 Controller​

package com.example.userservice.controller;

import com.example.userservice.model.User;
import com.example.userservice.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

private final UserService userService;

@Value("${server.port}")
private String serverPort;

public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}

@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

@GetMapping("/{id}/exists")
public ResponseEntity<Boolean> userExists(@PathVariable Long id) {
return ResponseEntity.ok(userService.existsById(id));
}

@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}

// Endpoint to see which instance responds
@GetMapping("/info")
public ResponseEntity<String> getInfo() {
return ResponseEntity.ok("Response from User Service on port: " + serverPort);
}
}

6. Load Balancing with Spring Cloud LoadBalancer​

6.1 Dependency​

<!-- Included in spring-cloud-starter-netflix-eureka-client -->
<!-- But can be added explicitly -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

6.2 RestTemplate with Load Balancing​

package com.example.orderservice.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
@LoadBalanced // Enables load balancing
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

6.3 Usage in a Service​

package com.example.orderservice.service;

import com.example.orderservice.model.User;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class UserServiceClient {

private final RestTemplate restTemplate;

public UserServiceClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

public User getUserById(Long userId) {
// Use the service name instead of the URL!
// Spring Cloud LoadBalancer resolves "user-service"
// to one of the instances registered in Eureka
String url = "http://user-service/api/users/" + userId;
return restTemplate.getForObject(url, User.class);
}

public boolean userExists(Long userId) {
String url = "http://user-service/api/users/" + userId + "/exists";
return Boolean.TRUE.equals(
restTemplate.getForObject(url, Boolean.class)
);
}
}

6.4 WebClient with Load Balancing (Reactive)​

package com.example.orderservice.config;

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
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
@Service
public class UserServiceClient {

private final WebClient.Builder webClientBuilder;

public UserServiceClient(WebClient.Builder webClientBuilder) {
this.webClientBuilder = webClientBuilder;
}

public Mono<User> getUserById(Long userId) {
return webClientBuilder.build()
.get()
.uri("http://user-service/api/users/" + userId)
.retrieve()
.bodyToMono(User.class);
}
}

7. Load Balancing Algorithms​

7.1 Round Robin (Default)​

Request 1 β†’ Instance A
Request 2 β†’ Instance B
Request 3 β†’ Instance C
Request 4 β†’ Instance A
...

7.2 Custom Configuration​

package com.example.orderservice.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class LoadBalancerConfig {

@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {

String name = environment.getProperty(
LoadBalancerClientFactory.PROPERTY_NAME
);

return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(
name,
ServiceInstanceListSupplier.class
),
name
);
}
}

7.3 Configuration by Service​

@Configuration
@LoadBalancerClient(name = "user-service", configuration = UserServiceLBConfig.class)
public class LoadBalancerConfiguration {
}

8. High Availability Eureka​

8.1 Problem: Single Point of Failure​

If Eureka Server goes down:
- New services cannot register
- Clients cannot discover new services
- BUT: the local cache continues to work

8.2 Solution: Eureka Cluster​

8.3 Configuration Eureka 1​

# eureka-server-1/application.yml
server:
port: 8761

spring:
application:
name: eureka-server
profiles:
active: peer1

eureka:
instance:
hostname: eureka1
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka2:8762/eureka/,http://eureka3:8763/eureka/

8.4 Configuration Eureka 2​

# eureka-server-2/application.yml
server:
port: 8762

spring:
application:
name: eureka-server
profiles:
active: peer2

eureka:
instance:
hostname: eureka2
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka1:8761/eureka/,http://eureka3:8763/eureka/

8.5 Client Configuration for Cluster​

eureka:
client:
service-url:
# List of all Eureka servers
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/,http://eureka3:8763/eureka/
# Automatic failover
eureka-server-connect-timeout-seconds: 5
eureka-server-read-timeout-seconds: 8

9. Self-Preservation Mode​

9.1 What is it?​

Eureka enters "self-preservation" mode when it detects a massive loss of heartbeats.

⚠️ EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP 
WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE
INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

9.2 Why?​

Scenario: Network issue between Eureka and services
- Eureka no longer receives heartbeats
- Without protection: Eureka removes all services
- Result: Disaster!

With Self-Preservation:
- Eureka detects the massive loss
- It keeps the services registered
- It waits for the network to return

9.3 Configuration​

eureka:
server:
# Enable/disable (default: true)
enable-self-preservation: true

# Renewal threshold (default: 0.85 = 85%)
renewal-percent-threshold: 0.85

# Threshold update interval
renewal-threshold-update-interval-ms: 900000

# In development, it can be disabled
# enable-self-preservation: false

10. Eureka REST API​

10.1 Main Endpoints​

EndpointMethodDescription
/eureka/appsGETList all services
/eureka/apps/\{appId\}GETService details
/eureka/apps/\{appId\}/\{instanceId\}GETInstance details
/eureka/apps/\{appId\}/\{instanceId\}DELETEUnregister
/eureka/apps/\{appId\}/\{instanceId\}/statusPUTChange status

10.2 Examples​

# List all services
curl http://localhost:8761/eureka/apps

# Get instances of user-service
curl http://localhost:8761/eureka/apps/USER-SERVICE

# Check status
curl http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081

# Set out of service (OUT_OF_SERVICE)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=OUT_OF_SERVICE"

# Set back in service (UP)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=UP"

10.3 XML Response​

<applications>
<versions__delta>1</versions__delta>
<apps__hashcode>UP_3_</apps__hashcode>
<application>
<name>USER-SERVICE</name>
<instance>
<instanceId>user-service:8081</instanceId>
<hostName>192.168.1.10</hostName>
<app>USER-SERVICE</app>
<ipAddr>192.168.1.10</ipAddr>
<status>UP</status>
<port enabled="true">8081</port>
<healthCheckUrl>http://192.168.1.10:8081/actuator/health</healthCheckUrl>
</instance>
</application>
</applications>

11. Best Practices​

11.1 Naming Convention​

# βœ… Good
spring:
application:
name: user-service

# ❌ Bad
spring:
application:
name: UserService
name: user_service
name: users

11.2 Health Checks​

eureka:
instance:
# Enable health checks
health-check-url-path: /actuator/health
status-page-url-path: /actuator/info

11.3 Graceful Shutdown​

@Component
public class GracefulShutdown implements DisposableBean {

@Autowired
private EurekaClient eurekaClient;

@Override
public void destroy() {
// Unregister cleanly
eurekaClient.shutdown();
}
}

Or via configuration:

server:
shutdown: graceful

spring:
lifecycle:
timeout-per-shutdown-phase: 30s

11.4 Appropriate Timeouts​

eureka:
instance:
# Heartbeat every 10 seconds
lease-renewal-interval-in-seconds: 10
# Expiration after 30 seconds without heartbeat
lease-expiration-duration-in-seconds: 30
client:
# Refresh registry every 5 seconds
registry-fetch-interval-seconds: 5

12. Summary​

Key Points​

βœ… Eureka Server = Central service registry
βœ… Eureka Client = Registers and discovers others
βœ… Load Balancing = Automatic with @LoadBalanced
βœ… Self-Preservation = Protection against false positives
βœ… High Availability = Cluster of Eureka servers

Configuration Checklist​

  • Eureka Server on port 8761
  • register-with-eureka: false for the server
  • spring.application.name defined for each client
  • Actuator for health checks
  • Load Balancer configured
  • Appropriate timeouts in production

13. Validation Quiz​

Question 1​

Why is @LoadBalanced necessary on RestTemplate?

See the answer

@LoadBalanced injects an interceptor that:

  1. Intercepts calls with a service name (e.g., http://user-service/...)
  2. Consults Eureka to get the instances
  3. Chooses an instance (round-robin by default)
  4. Replaces the name with the real IP:port
  5. Makes the HTTP call

Without @LoadBalanced, RestTemplate would try to resolve "user-service" as a normal DNS and fail.

Question 2​

What happens if Eureka Server goes down?

See the answer

Eureka clients keep a local cache of the registry:

  • Already known services continue to work
  • New services cannot register
  • Clients do not discover new instances
  • The system is in "degraded" mode but functional

That's why high availability (cluster) is important in production.


Next Chapter: Centralized Configuration β†’

Back to Module 13