Spring Boot
Service Registry
A service registry is a directory of available service instances. Services register on startup and deregister on shutdown. Clients query the registry to discover service locations instead of using hard-coded URLs. Spring Cloud provides Eureka (Netflix) as the default registry, with Consul and Zookeeper as alternatives. This entry covers Eureka Server setup, client registration, health checks, zone-aware routing, and self-preservation.
Eureka Server Setup
Create a dedicated Spring Boot application for the service registry. @EnableEurekaServer activates the Eureka dashboard and registration API. Run two or more Eureka servers in production and have them peer-replicate for high availability.
XML
<!-- Eureka Server pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
// ── Main application ───────────────────────────────────────────────────
@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
public static void main(String[] args) {
SpringApplication.run(
ServiceRegistryApplication.class, args);
}
}
# ── application.yml (standalone Eureka server) ────────────────────────
server:
port: 8761
spring:
application:
name: service-registry
security:
user:
name: ${EUREKA_USER:eureka}
password: ${EUREKA_PASSWORD}
eureka:
instance:
hostname: localhost
client:
# Standalone — do not register with itself
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
server:
# Wait before starting evictions — grace period for startup
wait-time-in-ms-when-sync-empty: 0
# Self-preservation: if > 85% of expected heartbeats arrive,
# do not evict instances. Adjust for your environment.
renewal-percent-threshold: 0.85
# ── Peer-aware Eureka (two instances for HA) ──────────────────────────
# application-peer1.yml
eureka:
instance:
hostname: eureka-peer1
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka:password@eureka-peer2:8761/eureka/
# application-peer2.yml
eureka:
instance:
hostname: eureka-peer2
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka:password@eureka-peer1:8761/eureka/Eureka Client Registration
Add spring-cloud-starter-netflix-eureka-client to each service. The service registers automatically on startup and sends heartbeats every 30 seconds by default. The Eureka dashboard at http://eureka-server:8761 shows all registered instances with their health status.
XML
<!-- Client service pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
// ── Auto-registration (no annotation needed in Spring Boot 3) ─────────
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(
OrderServiceApplication.class, args);
}
}
# ── application.yml (client service) ──────────────────────────────────
spring:
application:
name: order-service # service ID in Eureka — use lowercase
eureka:
client:
service-url:
defaultZone: >
http://${EUREKA_USER:eureka}:${EUREKA_PASSWORD}@
eureka-server:8761/eureka/
# Cache registry locally — reduces load on Eureka
registry-fetch-interval-seconds: 30
# Prefer IP over hostname for containerised environments
instance:
prefer-ip-address: true
instance:
# Unique instance ID (important for multiple instances)
instance-id: >
${spring.application.name}:
${spring.cloud.client.ip-address}:
${server.port}
# How often to send heartbeats (default 30s)
lease-renewal-interval-in-seconds: 15
# Time before Eureka removes instance without heartbeat
lease-expiration-duration-in-seconds: 45
# Health check URL — Eureka uses actuator/health
health-check-url-path: /actuator/health
status-page-url-path: /actuator/info
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: alwaysService Discovery with DiscoveryClient
Use DiscoveryClient to programmatically query the registry for service instances. Spring Cloud LoadBalancer uses the registry to resolve service names to URLs for @LoadBalanced RestClient and WebClient calls. Service names replace hard-coded host:port pairs.
Java
// ── DiscoveryClient — programmatic service lookup ─────────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class ServiceDiscoveryDemo {
private final DiscoveryClient discoveryClient;
// ── List all registered services ──────────────────────────────────
public List<String> listServices() {
return discoveryClient.getServices();
}
// ── Get instances of a specific service ───────────────────────────
public List<ServiceInstance> getInstances(String serviceId) {
List<ServiceInstance> instances =
discoveryClient.getInstances(serviceId);
instances.forEach(instance ->
log.info("Service: {} at {}:{} meta={}",
instance.getServiceId(),
instance.getHost(),
instance.getPort(),
instance.getMetadata()));
return instances;
}
// ── Pick one instance (for manual load balancing) ─────────────────
public Optional<String> resolveUrl(String serviceId) {
return discoveryClient.getInstances(serviceId)
.stream()
.findFirst()
.map(ServiceInstance::getUri)
.map(URI::toString);
}
}
// ── @LoadBalanced RestClient — resolves service name via registry ──────
@Configuration
public class RestClientConfig {
@Bean
@LoadBalanced // Spring Cloud LoadBalancer intercepts calls
public RestClient.Builder loadBalancedRestClientBuilder() {
return RestClient.builder();
}
@Bean
public RestClient inventoryClient(
@LoadBalanced RestClient.Builder builder) {
return builder
.baseUrl("http://inventory-service") // service name, not URL
.build();
}
}
// ── Service using load-balanced RestClient ─────────────────────────────
@Service
@RequiredArgsConstructor
public class InventoryClient {
private final RestClient inventoryClient;
public StockResponse checkStock(Long productId) {
// "inventory-service" resolved to actual host:port via Eureka
return inventoryClient.get()
.uri("/api/v1/stock/{id}", productId)
.retrieve()
.body(StockResponse.class);
}
}
// ── @LoadBalanced WebClient ────────────────────────────────────────────
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
@Bean
public WebClient paymentClient(
@LoadBalanced WebClient.Builder builder) {
return builder
.baseUrl("http://payment-service")
.build();
}
}Health Checks and Metadata
Eureka marks instances UP or DOWN based on actuator health checks. Custom health indicators contribute to the overall health and control whether the instance receives traffic. Instance metadata carries custom key-value pairs — zone, version, capabilities — that clients can use for routing decisions.
Java
// ── Custom health indicator ───────────────────────────────────────────
@Component
@RequiredArgsConstructor
public class DatabaseHealthIndicator
implements HealthIndicator {
private final DataSource dataSource;
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
conn.isValid(3);
return Health.up()
.withDetail("database", "PostgreSQL")
.withDetail("status", "connected")
.build();
} catch (SQLException ex) {
return Health.down()
.withDetail("error", ex.getMessage())
.build();
}
}
}
// ── Composite health — all indicators must be UP ──────────────────────
// If any indicator returns DOWN, Eureka marks the instance OUT_OF_SERVICE
// and load balancer stops routing traffic to it.
// ── Instance metadata for zone-aware routing ──────────────────────────
eureka:
instance:
metadata-map:
zone: us-east-1a # availability zone
version: "2.1.0" # service version
weight: "100" # relative weight for load balancing
// ── Read metadata in a custom load balancer ───────────────────────────
@Component
@RequiredArgsConstructor
@Slf4j
public class ZoneAwareServiceInstanceListSupplier
implements ServiceInstanceListSupplier {
private final ServiceInstanceListSupplier delegate;
@Value("${eureka.instance.metadata-map.zone:default}")
private String currentZone;
@Override
public String getServiceId() {
return delegate.getServiceId();
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get().map(instances -> {
// Prefer instances in the same zone
List<ServiceInstance> sameZone = instances.stream()
.filter(i -> currentZone.equals(
i.getMetadata().get("zone")))
.toList();
return sameZone.isEmpty() ? instances : sameZone;
});
}
}
# ── Self-preservation tuning ──────────────────────────────────────────
eureka:
server:
# Disable self-preservation in development
# (instances removed immediately on missed heartbeats)
enable-self-preservation: false # dev only
eviction-interval-timer-in-ms: 5000
# In production keep self-preservation ON
# to prevent mass eviction during network partitions
enable-self-preservation: true # prod (default)
renewal-percent-threshold: 0.85Consul as an Alternative Registry
HashiCorp Consul provides service discovery, health checking, and key-value configuration in a single tool. Spring Cloud Consul integrates with the same @LoadBalanced abstractions as Eureka — switching requires only a dependency swap and configuration change, no code changes.
XML
<!-- Replace Eureka client with Consul client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
# ── application.yml (Consul client) ──────────────────────────────────
spring:
application:
name: order-service
cloud:
consul:
host: ${CONSUL_HOST:localhost}
port: ${CONSUL_PORT:8500}
token: ${CONSUL_TOKEN:} # ACL token for secure Consul
discovery:
enabled: true
prefer-ip-address: true
instance-id: >
${spring.application.name}-
${spring.cloud.client.ip-address}-
${server.port}
health-check-path: /actuator/health
health-check-interval: 15s
health-check-timeout: 10s
deregister-critical-services-after: 30s
tags:
- version=${app.version:1.0.0}
- zone=${AVAILABILITY_ZONE:default}
config:
enabled: true
prefix: config
default-context: application
profile-separator: ","
format: YAML
# Reads from Consul KV at:
# config/order-service/data
# config/application/data (shared)
# ── Consul Docker Compose ─────────────────────────────────────────────
# services:
# consul:
# image: consul:1.17
# command: >
# agent -server -bootstrap-expect=1
# -ui -client=0.0.0.0
# ports:
# - "8500:8500"
# - "8600:8600/udp"