Service Discovery & Registry

Difficulty: Advanced

Question

How do microservices find each other? Explain service discovery patterns and their trade-offs.

Answer

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.

Code examples

Service Registry Implementation

// 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.

Client-Side Discovery Pattern

// 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 Service Discovery

// 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.

Key points

Concepts covered

Service Discovery, Service Registry, Health Checks, DNS-Based Discovery, Client-Side vs Server-Side