Difficulty: Advanced
How do microservices find each other? Explain service discovery patterns and their trade-offs.
Service discovery is the mechanism by which services locate each other in a distributed system where instances are dynamic (scaling, deployments, failures).
Patterns: 1. Client-Side Discovery: Client queries service registry, picks an instance, sends request directly. (Netflix Eureka) 2. Server-Side Discovery: Client sends request to a load balancer/router, which queries the registry. (AWS ALB, Kubernetes) 3. DNS-Based: Services register DNS records. Simple but limited (TTL caching issues, no health status). (AWS Cloud Map)
Service Registry: A database of available service instances with their network locations. - Registration: Services self-register on startup, deregister on shutdown - Health checks: Registry periodically checks instance health - Examples: Consul, etcd, ZooKeeper, Kubernetes built-in
Kubernetes approach: Built-in service discovery via DNS. Each Service gets a DNS name (my-service.namespace.svc.cluster.local) that resolves to healthy pod IPs.
// Simple service registry
class ServiceRegistry {
constructor() {
this.services = new Map(); // serviceName -> Map<instanceId, instance>
this.startHealthChecks();
}
register(serviceName, instanceId, metadata) {
if (!this.services.has(serviceName)) {
this.services.set(serviceName, new Map());
}
this.services.get(serviceName).set(instanceId, {
instanceId,
host: metadata.host,
port: metadata.port,
status: 'UP',
lastHeartbeat: Date.now(),
metadata
});
console.log(`Registered: ${serviceName}/${instanceId}`);
}
deregister(serviceName, instanceId) {
this.services.get(serviceName)?.delete(instanceId);
}
getInstances(serviceName) {
const instances = this.services.get(serviceName);
if (!instances) return [];
return [...instances.values()].filter(i => i.status === 'UP');
}
heartbeat(serviceName, instanceId) {
const instance = this.services.get(serviceName)?.get(instanceId);
if (instance) instance.lastHeartbeat = Date.now();
}
startHealthChecks() {
setInterval(() => {
for (const [name, instances] of this.services) {
for (const [id, instance] of instances) {
if (Date.now() - instance.lastHeartbeat > 30000) {
instance.status = 'DOWN';
console.log(`Instance down: ${name}/${id}`);
}
}
}
}, 10000);
}
}
// Registry API
app.post('/registry/register', (req, res) => {
registry.register(req.body.service, req.body.instanceId, req.body);
res.json({ status: 'registered' });
});
app.get('/registry/services/:name', (req, res) => {
res.json(registry.getInstances(req.params.name));
});
The service registry tracks all available instances and their health status. Instances send heartbeats every few seconds. If heartbeats stop, the instance is marked as DOWN and removed from routing.
// Service client with built-in discovery and load balancing
class ServiceClient {
constructor(serviceName, registry) {
this.serviceName = serviceName;
this.registry = registry;
this.instanceCache = [];
this.currentIndex = 0;
// Refresh cache every 10 seconds
setInterval(() => this.refreshCache(), 10000);
this.refreshCache();
}
async refreshCache() {
try {
const response = await fetch(
`${this.registry}/registry/services/${this.serviceName}`
);
this.instanceCache = await response.json();
} catch (error) {
console.error('Registry refresh failed, using cached instances');
}
}
// Round-robin load balancing
getNextInstance() {
if (this.instanceCache.length === 0) {
throw new Error(`No instances available for ${this.serviceName}`);
}
const instance = this.instanceCache[
this.currentIndex % this.instanceCache.length
];
this.currentIndex++;
return instance;
}
async request(path, options = {}) {
const maxRetries = 3;
let lastError;
for (let i = 0; i < maxRetries; i++) {
const instance = this.getNextInstance();
const url = `http://${instance.host}:${instance.port}${path}`;
try {
const response = await fetch(url, {
...options,
timeout: 5000
});
return response;
} catch (error) {
lastError = error;
console.error(`Request failed to ${url}, retrying...`);
}
}
throw lastError;
}
}
// Usage in order service
const paymentClient = new ServiceClient('payment-service', REGISTRY_URL);
const result = await paymentClient.request('/charge', {
method: 'POST',
body: JSON.stringify({ amount: 99.99 })
});
Client-side discovery embeds the registry lookup and load balancing in the calling service. The client caches instance lists and handles retries with failover to other instances.
// Kubernetes handles service discovery natively
// 1. Deploy a service (deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
replicas: 3
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
spec:
containers:
- name: payment
image: myapp/payment-service:1.0
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
httpGet:
path: /ready
port: 3000
---
// 2. Create a Service (service.yaml)
apiVersion: v1
kind: Service
metadata:
name: payment-service
spec:
selector:
app: payment-service
ports:
- port: 80
targetPort: 3000
// 3. Other services call it by DNS name:
// http://payment-service/charge
// Kubernetes DNS automatically resolves to healthy pod IPs
// Built-in load balancing across all ready pods
// No external registry needed!
// Kubernetes API server IS the service registry
// kube-proxy handles load balancing
// Probes handle health checking
Kubernetes provides built-in service discovery through DNS. Each Service gets a stable DNS name that resolves to healthy pods. Liveness probes restart unhealthy pods; readiness probes remove them from routing.
Service Discovery, Service Registry, Health Checks, DNS-Based Discovery, Client-Side vs Server-Side