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β
| Concept | Description |
|---|---|
| Register | Service registers at startup |
| Renew (Heartbeat) | Ping every 30 seconds |
| Fetch Registry | Client retrieves the list of services |
| Cancel | Service unregisters on shutdown |
| Eviction | Eureka 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β
| Endpoint | Method | Description |
|---|---|---|
/eureka/apps | GET | List all services |
/eureka/apps/\{appId\} | GET | Service details |
/eureka/apps/\{appId\}/\{instanceId\} | GET | Instance details |
/eureka/apps/\{appId\}/\{instanceId\} | DELETE | Unregister |
/eureka/apps/\{appId\}/\{instanceId\}/status | PUT | Change 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: falsefor the server -
spring.application.namedefined 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:
- Intercepts calls with a service name (e.g.,
http://user-service/...) - Consults Eureka to get the instances
- Chooses an instance (round-robin by default)
- Replaces the name with the real IP:port
- 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.