Microservices: Service Discovery
Implement service registration and dynamic discovery with health checks in microservices.
When to Use
- Services are deployed dynamically (Kubernetes, ECS, auto-scaling groups) and their IPs change
- You need services to find each other without hardcoded hostnames
- You want health-check-aware routing — only route to healthy instances
- You're scaling services horizontally and need load balancing across instances
Instructions
Kubernetes DNS-based discovery (most common — no code needed):
# Kubernetes handles discovery automatically via DNS
# Service A calls Service B using the DNS name: http://order-service:8080
# Kubernetes resolves this to the correct pod IPs
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: production
spec:
selector:
app: order-service
ports:
- port: 8080
targetPort: 8080
type: ClusterIP # internal only
---
# In your app code — just use the DNS name
const ORDER_SERVICE_URL = process.env.ORDER_SERVICE_URL ?? 'http://order-service:8080';
Consul-based service registration (for non-Kubernetes environments):
import Consul from 'consul';
const consul = new Consul({ host: process.env.CONSUL_HOST ?? 'consul' });
async function registerService(): Promise<void> {
const serviceId = `order-service-${process.env.POD_NAME ?? process.env.HOSTNAME}`;
await consul.agent.service.register({
id: serviceId,
name: 'order-service',
port: parseInt(process.env.PORT ?? '8080'),
address: process.env.POD_IP ?? '0.0.0.0',
tags: ['v1', 'production'],
check: {
http: `http://${process.env.POD_IP}:${process.env.PORT}/health`,
interval: '10s',
timeout: '5s',
deregistercriticalserviceafter: '30s',
},
});
console.log(`Registered service: ${serviceId}`);
// Deregister gracefully on shutdown
const cleanup = async () => {
await consul.agent.service.deregister(serviceId);
process.exit(0);
};
process.on('SIGTERM', cleanup);
process.on('SIGINT', cleanup);
}
// Client-side discovery — resolve service address from Consul
async function resolveService(name: string): Promise<string> {
const services = await consul.health.service({
service: name,
passing: true, // only healthy instances
});
if (services.length === 0) throw new Error(`No healthy instances of ${name}`);
// Simple round-robin
const instance = services[Math.floor(Math.random() * services.length)];
return `http://${instance.Service.Address}:${instance.Service.Port}`;
}
// Usage
const orderServiceUrl = await resolveService('order-service');
const order = await fetch(`${orderServiceUrl}/orders/${orderId}`);
Service discovery with caching (avoid per-request lookup):
class ServiceRegistry {
private cache = new Map<string, { url: string; expiresAt: number }>();
private readonly ttlMs = 30_000; // cache for 30 seconds
async resolve(serviceName: string): Promise<string> {
const cached = this.cache.get(serviceName);
if (cached && Date.now() < cached.expiresAt) return cached.url;
const url = await this.lookup(serviceName);
this.cache.set(serviceName, { url, expiresAt: Date.now() + this.ttlMs });
return url;
}
private async lookup(name: string): Promise<string> {
// Could be Consul, etcd, or DNS
return resolveService(name);
}
invalidate(serviceName: string): void {
this.cache.delete(serviceName);
}
}
const registry = new ServiceRegistry();
// Use in HTTP client
async function callOrderService(path: string): Promise<Response> {
const baseUrl = await registry.resolve('order-service');
const response = await fetch(`${baseUrl}${path}`);
if (response.status === 503) {
// Service may have moved — invalidate cache and retry once
registry.invalidate('order-service');
const freshUrl = await registry.resolve('order-service');
return fetch(`${freshUrl}${path}`);
}
return response;
}
Details
Client-side vs. server-side discovery:
| Client-Side | Server-Side | |
|---|---|---|
| Who resolves | Each service | Load balancer / gateway |
| Example | Consul + client library | AWS ALB, Kubernetes Service |
| Complexity | Higher (each service implements) | Lower (transparent) |
| Flexibility | More control | Less control |
In Kubernetes, prefer server-side discovery — Services and DNS handle it for you.
Health checks are required: Discovery without health checks routes traffic to dead instances. Register health check endpoints and ensure Consul/k8s removes unhealthy instances quickly (see microservices-health-check skill).
Anti-patterns:
- Hardcoding service IPs or hostnames — breaks on any deployment change
- Resolving service addresses on every request without caching — hammers the registry
- No graceful deregistration on shutdown — registry keeps routing to the dead instance until health check catches it
Environment variable pattern for configuration:
// Use environment variables for service URLs in all environments
// Development: http://localhost:8081
// Staging/Production: http://order-service (Kubernetes DNS)
const ORDER_SERVICE_URL = process.env.ORDER_SERVICE_URL;
if (!ORDER_SERVICE_URL) throw new Error('ORDER_SERVICE_URL is required');
Source
microservices.io/patterns/service-registry.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.