Setting Up Service Discovery for Microservices
Service Discovery enables microservices to find each other dynamically, without hardcoded IP addresses. When Order Service needs to call Payment Service, it queries the registry: "where is payment-service right now?"—and gets the current address.
Client-Side vs Server-Side Discovery
Client-Side: a service queries the registry itself and selects an instance (with client-side load balancing). Example — Eureka + Ribbon in Spring Cloud.
Server-Side: a service calls a load balancer, which consults the registry. Example — Kubernetes DNS + Service, AWS ELB.
Tools Overview
| Tool | Approach | Integration |
|---|---|---|
| Kubernetes DNS | Server-side | Native for K8s |
| Consul | Client/Server-side | Any stack |
| Eureka (Netflix OSS) | Client-side | Spring Cloud |
| etcd | KV + watch | Kubernetes, CoreDNS |
In Kubernetes, the built-in DNS solves most tasks—it knows every service by name: service-name.namespace.svc.cluster.local.
Consul Service Discovery
# consul-agent.hcl
datacenter = "dc1"
data_dir = "/opt/consul"
log_level = "INFO"
server = false
retry_join = ["consul-server:8300"]
# Health check every 10 sec
check = {
id = "order-service-health"
name = "Order Service Health"
http = "http://localhost:3000/health"
interval = "10s"
timeout = "3s"
}
Service registration via API:
import Consul from 'consul';
const consul = new Consul({ host: process.env.CONSUL_HOST });
async function registerService() {
await consul.agent.service.register({
name: 'order-service',
id: `order-service-${process.env.POD_NAME}`,
address: process.env.POD_IP,
port: 3000,
tags: ['v1', 'production'],
check: {
http: `http://${process.env.POD_IP}:3000/health`,
interval: '10s',
deregisterCriticalServiceAfter: '1m'
}
});
}
// Deregister on shutdown
process.on('SIGTERM', async () => {
await consul.agent.service.deregister(`order-service-${process.env.POD_NAME}`);
process.exit(0);
});
Service lookup and invocation:
async function getPaymentServiceUrl(): Promise<string> {
const services = await consul.health.service({
service: 'payment-service',
passing: true // only healthy instances
});
if (services.length === 0) {
throw new ServiceUnavailableError('payment-service');
}
// Round-robin load balancing
const instance = services[Math.floor(Math.random() * services.length)];
return `http://${instance.Service.Address}:${instance.Service.Port}`;
}
Kubernetes DNS (Recommended for K8s)
Kubernetes doesn't need a separate discovery—each Service gets a DNS record:
apiVersion: v1
kind: Service
metadata:
name: payment-service
namespace: production
spec:
selector:
app: payment-service
ports:
- port: 80
targetPort: 3000
Now from any pod: http://payment-service.production.svc.cluster.local/charge or simply http://payment-service in the same namespace.
Headless Service for direct access to pods (StatefulSet):
spec:
clusterIP: None # headless
selector:
app: kafka
DNS returns an A-record for each pod: kafka-0.kafka.production.svc.cluster.local.
Health Checks
A service must respond to /health or /readiness:
app.get('/health', (req, res) => {
const checks = {
database: dbPool.totalCount > 0 ? 'ok' : 'error',
redis: redisClient.isReady ? 'ok' : 'error',
uptime: process.uptime()
};
const healthy = Object.values(checks).every(v => v === 'ok' || typeof v === 'number');
res.status(healthy ? 200 : 503).json({ status: healthy ? 'ok' : 'degraded', checks });
});
Implementation Timeline
- Service Discovery with Consul + registration/deregistration — 3–5 days
- Kubernetes-native approach with proper Health Checks — 1–2 days







