Service Discovery¶
Service discovery is the automatic detection and registration of services in a distributed system. Instead of hardcoding service locations, services register themselves and clients discover them dynamically.
Problem: Static Service Locations¶
// ❌ Bad: Hardcoded URLs break when services move
@Configuration
public class PaymentClientConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public String getPaymentServiceUrl() {
return "http://payment-service-prod-1:8080"; // Fixed IP/hostname
}
}
Issues: - Services move (scaling, failures, deployments) - No awareness of instance health - Manual configuration maintenance - Difficult to handle load balancing
Solution: Service Discovery¶
Service A (registers itself)
↓
Discovery Server (Eureka, Consul, etcd)
↑
Service B (queries "where is Service A?")
↓
Discovery Server responds with: [ip:port list]
↓
Service B uses endpoint dynamically
Service Discovery Patterns¶
1. Client-Side Discovery¶
Client queries registry → gets service instances → chooses one → calls it
@Service
public class PaymentClientService {
@Autowired
private DiscoveryClient discoveryClient;
public PaymentResponse chargeCard(Order order) {
// Query registry for all "payment-service" instances
List<ServiceInstance> instances =
discoveryClient.getInstances("payment-service");
if (instances.isEmpty()) {
throw new ServiceUnavailableException("Payment service down");
}
// Client chooses an instance (round-robin, random, etc.)
ServiceInstance instance = instances.get(0);
String url = instance.getUri().toString();
// Call the service
return restTemplate.postForObject(
url + "/api/charge",
order,
PaymentResponse.class
);
}
}
Pros: - Client has control over load balancing logic - Can implement smart routing (latency-aware, circuit breaker)
Cons: - Client logic is complex - Every client must implement service discovery
2. Server-Side Discovery¶
Client calls a load balancer → load balancer queries registry → routes request
Client
↓ (calls load balancer)
API Gateway / Load Balancer
↓ (queries registry for service location)
Discovery Server (Eureka, Consul)
↓ (responds with available instances)
Load Balancer
↓ (routes to chosen instance)
Service A / Service B / Service C
Pros: - Client simplicity (just call load balancer URL) - Centralized routing logic
Cons: - Load balancer becomes single point of failure - Requires stable load balancer endpoint
Spring Cloud Eureka (Client-Side Discovery)¶
Setup¶
Dependencies:
<!-- Server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- Clients -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Eureka Server Configuration¶
# Eureka Server (application.yml)
spring:
application:
name: eureka-server
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
server:
enableSelfPreservation: false # Dev only; prod should be true
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Eureka Client (Service Registration)¶
# application.yml
spring:
application:
name: payment-service
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${server.port}
preferIpAddress: true
leaseRenewalIntervalInSeconds: 30 # Heartbeat every 30s
leaseExpirationDurationInSeconds: 90 # Remove if no heartbeat for 90s
@SpringBootApplication
@EnableEurekaClient
public class PaymentServiceApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentServiceApplication.class, args);
}
}
@RestController
@RequestMapping("/api")
public class PaymentController {
@PostMapping("/charge")
public PaymentResponse charge(@RequestBody Order order) {
// Service logic
return new PaymentResponse(order.getId(), "SUCCESS");
}
}
Eureka Client (Service Discovery)¶
@Service
public class OrderService {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
public Order createOrder(Order order) {
// Discover payment service
ServiceInstance paymentService =
discoveryClient.getInstances("payment-service")
.stream()
.findAny()
.orElseThrow(() -> new ServiceUnavailableException("Payment service"));
String paymentUrl = paymentService.getUri().toString();
// Charge order
PaymentResponse payment = restTemplate.postForObject(
paymentUrl + "/api/charge",
order,
PaymentResponse.class
);
if ("SUCCESS".equals(payment.getStatus())) {
order.setStatus("CONFIRMED");
}
return order;
}
}
Better: Spring Cloud LoadBalancer¶
Instead of manual DiscoveryClient calls, use @LoadBalanced RestTemplate:
@Configuration
public class ClientConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
public class OrderService {
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
public Order createOrder(Order order) {
// Use service name directly; load balancer resolves it
PaymentResponse payment = restTemplate.postForObject(
"http://payment-service/api/charge", // No IP/port!
order,
PaymentResponse.class
);
order.setStatus("CONFIRMED");
return order;
}
}
Service Discovery Frameworks¶
| Framework | Type | Status | Approach | Use Case |
|---|---|---|---|---|
| Eureka | Library | ⚠️ Maintenance | Client-side discovery, registry | Netflix stack, Spring Cloud |
| Consul | Standalone | ✅ Active | Server + client-side, KV store | Polyglot microservices, multi-cloud |
| etcd | Standalone | ✅ Active | Distributed key-value store | Kubernetes, CoreOS, cloud-native |
| Zookeeper | Standalone | ⚠️ Legacy | Distributed coordination | Hadoop, Kafka, older systems |
| Kubernetes DNS | Built-in | ✅ Native | Server-side via DNS | Container-native services |
Detailed Comparison¶
Eureka (Netflix)¶
// ✅ Pros
- Spring Cloud native integration
- Easy to set up with Spring Boot
- Client-side load balancing
// ❌ Cons
- Maintenance mode (Netflix moved to Kubernetes)
- Less suitable for polyglot environments
- Requires Java
Consul (HashiCorp)¶
// ✅ Pros
- HTTP/DNS interfaces (language-agnostic)
- Built-in health checks and KV store
- Server-side discovery via DNS
- Multi-datacenter support
// ❌ Cons
- More complex to operate
- Heavier than Eureka
Kubernetes DNS (Native)¶
# In Kubernetes, services auto-discover via DNS
# Pod can call another service directly:
http://payment-service.default.svc.cluster.local:8080
# ✅ Pros
- Zero configuration (declarative)
- Native container orchestration
- Service health managed by K8s
# ❌ Cons
- Tightly coupled to Kubernetes
- Not suitable for non-containerized deployments
Health Checks & Heartbeat¶
Service discovery relies on health information to avoid routing to dead instances.
Eureka Health Check¶
eureka:
instance:
leaseRenewalIntervalInSeconds: 30 # Heartbeat interval
leaseExpirationDurationInSeconds: 90 # TTL before removal
@Component
public class HealthCheck {
@GetMapping("/actuator/health")
public HealthStatus health() {
return new HealthStatus("UP", "Database: UP, Cache: UP");
}
}
Consul Health Check¶
{
"id": "payment-service-1",
"name": "payment-service",
"tags": ["payment", "production"],
"address": "192.168.1.100",
"port": 8080,
"check": {
"http": "http://localhost:8080/health",
"interval": "10s",
"timeout": "5s"
}
}
Failure Scenarios & Mitigation¶
| Scenario | Impact | Mitigation |
|---|---|---|
| Service crashes | Registry still lists it (stale entry) | Health checks + TTL + circuit breaker |
| Registry down | New services can't register | Cache discovered services locally, fallback IPs |
| Network partition | Service isolated, registry can't reach it | Quorum-based removal (Consul), self-preservation (Eureka) |
| Client cache stale | Routes to dead instance | Combine with retry + circuit breaker |
Resilient Discovery Pattern¶
@Service
public class ResilientPaymentClient {
@Autowired
private DiscoveryClient discoveryClient;
@CircuitBreaker(name = "payment")
@Retry(name = "payment")
public PaymentResponse charge(Order order) {
ServiceInstance instance = discoveryClient
.getInstances("payment-service")
.stream()
.findAny()
.orElseThrow(() -> new ServiceUnavailableException("No healthy instances"));
try {
return restTemplate.postForObject(
instance.getUri() + "/api/charge",
order,
PaymentResponse.class
);
} catch (Exception e) {
// Circuit breaker + retry will handle
throw e;
}
}
}
Interview Talking Points¶
- Why service discovery? Eliminate hardcoded URLs; support dynamic scaling, failover, rolling updates
- Client vs. server discovery? Client-side = flexible routing, server-side = simpler clients
- Eureka vs. Kubernetes DNS? Eureka for legacy/polyglot; K8s DNS for containerized
- Health checks importance? Prevent routing to dead instances; combine with circuit breaker
- Failure handling? Use retry + circuit breaker + fallback; never trust discovery alone
- Scalability? Service discovery is a bottleneck; cache results, use hierarchical registries
See also: Spring Cloud Implementation, Microservices Principles