Service Discovery Patterns: How Microservices Find Each Other
In a monolith, calling another module is a function call. In microservices, it is a network call to a moving target. Services scale up, scale down, crash, and redeploy — their IP addresses and ports change constantly. Service discovery solves the question: where is the instance I need to talk to right now?
The Core Problem#
Without discovery, you hardcode addresses:
ORDER_SERVICE=http://10.0.1.5:3000
USER_SERVICE=http://10.0.1.6:3001
This breaks when:
- A service restarts on a different port
- Auto-scaling adds new instances
- A deployment rolls out to new IPs
- A node fails and workloads migrate
Service discovery automates the mapping from logical name to network location.
Client-Side Discovery#
The client queries a service registry directly and picks an instance.
1. Order Service asks registry: "Where is User Service?"
2. Registry returns: [10.0.1.6:3001, 10.0.2.8:3001, 10.0.3.2:3001]
3. Order Service picks one (round-robin, least connections, etc.)
4. Order Service calls 10.0.2.8:3001 directly
Advantages:
- No extra network hop (client talks directly to the target)
- Client can implement smart load balancing
- Lower latency
Disadvantages:
- Every client needs discovery logic
- Language-specific client libraries required
- Tighter coupling between client and registry
Examples: Netflix Eureka + Ribbon, gRPC client-side load balancing.
Server-Side Discovery#
The client calls a load balancer or router that handles discovery internally.
1. Order Service calls http://user-service/api/users
2. Load balancer queries registry for User Service instances
3. Load balancer routes to 10.0.2.8:3001
4. Response flows back through load balancer
Advantages:
- Clients stay simple (just call a DNS name)
- Language-agnostic — any HTTP client works
- Centralized load balancing logic
Disadvantages:
- Extra network hop through the load balancer
- Load balancer becomes a potential bottleneck
- Higher latency than client-side
Examples: AWS ALB, Kubernetes Services, NGINX with Consul.
Service Registry#
The registry is the source of truth for which instances are alive and where they live.
How Registration Works#
Self-registration: Services register themselves on startup and send heartbeats.
// On startup
await registry.register({
name: "user-service",
address: "10.0.2.8",
port: 3001,
healthCheck: "/health"
});
// Every 10 seconds
await registry.heartbeat("user-service", instanceId);
// On shutdown
await registry.deregister("user-service", instanceId);
Third-party registration: A separate process (registrar) watches for new instances and registers them.
Platform (K8s, ECS) starts container
-> Registrar detects new container
-> Registrar adds entry to registry
-> Registrar removes entry when container stops
Popular Registries#
| Registry | Consensus | Health Checks | Key Feature |
|---|---|---|---|
| Consul | Raft | HTTP, TCP, gRPC, script | Multi-datacenter, service mesh |
| etcd | Raft | TTL-based leases | Kubernetes backing store |
| ZooKeeper | ZAB | Ephemeral nodes | Mature, battle-tested |
| Eureka | Peer replication | Client heartbeat | Spring Cloud native |
Consul Example#
service {
name = "user-service"
port = 3001
tags = ["v2", "primary"]
check {
http = "http://localhost:3001/health"
interval = "10s"
timeout = "2s"
}
}
Query healthy instances:
GET /v1/health/service/user-service?passing=true
etcd with Leases#
# Create a lease (TTL 30s)
etcdctl lease grant 30
# lease 694d7550e3b3d101 granted with TTL(30s)
# Register with lease
etcdctl put /services/user-service/instance-1 \
'{"address":"10.0.2.8","port":3001}' \
--lease=694d7550e3b3d101
# Keep alive (renew before TTL expires)
etcdctl lease keep-alive 694d7550e3b3d101
If the service crashes, the lease expires and the key is automatically deleted.
DNS-Based Discovery#
The simplest form: use DNS to resolve service names to IPs.
dig user-service.internal A
;; ANSWER SECTION:
user-service.internal. 30 IN A 10.0.2.8
user-service.internal. 30 IN A 10.0.3.2
SRV records add port information:
_user-service._tcp.internal. 30 IN SRV 0 50 3001 10.0.2.8.
_user-service._tcp.internal. 30 IN SRV 0 50 3001 10.0.3.2.
Advantages: Universal — every language has a DNS client.
Limitations:
- DNS caching causes stale entries (TTL trade-off)
- No built-in health checking
- Limited metadata (no tags, versions, weights)
Tools: CoreDNS, AWS Route 53, Consul DNS interface.
Health Checking#
Discovery without health checking sends traffic to dead instances.
Levels of Health Checks#
| Level | What it checks | Example |
|---|---|---|
| Liveness | Is the process running? | TCP connect on port 3001 |
| Readiness | Can it handle requests? | HTTP 200 on /ready |
| Deep health | Are dependencies healthy? | DB connected, cache reachable |
Implementation Pattern#
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});
app.get("/ready", async (req, res) => {
const dbOk = await db.ping().catch(() => false);
const cacheOk = await redis.ping().catch(() => false);
if (dbOk && cacheOk) {
res.status(200).json({ status: "ready", db: true, cache: true });
} else {
res.status(503).json({ status: "not ready", db: dbOk, cache: cacheOk });
}
});
Important: Liveness checks should be lightweight. Heavy liveness checks can cause cascading restarts.
Service Mesh Discovery#
A service mesh (Istio, Linkerd, Consul Connect) moves discovery into the infrastructure layer.
[Order Service] -> [Sidecar Proxy] -> [Sidecar Proxy] -> [User Service]
The sidecar proxy handles:
- Service discovery (where to route)
- Load balancing (which instance)
- Health checking (remove unhealthy)
- mTLS (encrypt in transit)
- Retries and circuit breaking
Advantage: Application code has zero discovery logic. Just call http://user-service:3001.
Cost: Extra memory and CPU per pod, operational complexity of the mesh.
Kubernetes DNS#
Kubernetes has built-in service discovery via DNS.
ClusterIP Services#
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: production
spec:
selector:
app: user-service
ports:
- port: 3001
Every pod can now resolve:
user-service.production.svc.cluster.local -> 10.96.0.15 (ClusterIP)
Kubernetes handles load balancing across healthy pods via iptables/IPVS rules.
Headless Services (Direct Pod Discovery)#
spec:
clusterIP: None # headless
selector:
app: user-service
DNS returns individual pod IPs — useful for stateful workloads (databases, caches) where clients need to reach specific pods.
External Services#
kind: Service
spec:
type: ExternalName
externalName: db.example.com
Map external dependencies into the Kubernetes DNS namespace for uniform discovery.
Choosing the Right Pattern#
| Scenario | Recommended Pattern |
|---|---|
| Kubernetes-native apps | Kubernetes DNS + Services |
| Multi-cloud or hybrid | Consul with DNS interface |
| Spring Boot microservices | Eureka + Spring Cloud |
| High-performance gRPC | Client-side with xDS |
| Zero-trust security needed | Service mesh (Istio/Linkerd) |
| Simple internal services | DNS with health-checked ALB |
Key Takeaways#
- Service discovery maps logical names to live network addresses
- Client-side discovery is faster; server-side discovery is simpler
- Health checking is non-negotiable — never route to dead instances
- Kubernetes DNS solves discovery for most containerized workloads
- Service meshes push discovery into infrastructure, keeping app code clean
- Start with the simplest option that meets your needs — do not over-engineer
Article #248 in the System Design series. Keep building: codelit.io/blog.
Try these templates
Scalable SaaS Application
Modern SaaS with microservices, event-driven processing, and multi-tenant architecture.
10 componentsURL Shortener Service
Scalable URL shortening with analytics, custom aliases, and expiration.
7 componentsKubernetes Container Orchestration
K8s cluster with pod scheduling, service mesh, auto-scaling, and CI/CD deployment pipeline.
9 componentsContinue learning
Go deeper on microservices and integration
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
Building Microservices
Sam Newman · 2021
600 pages on splitting a monolith: service boundaries, data decomposition, migration order.
Microservices Patterns
Chris Richardson · 2018
Sagas as running Java code: compensating transactions, outbox, queries across services.
4.6 (705)Enterprise Integration Patterns
Gregor Hohpe, Bobby Woolf · 2003
A catalog of 65 messaging patterns with diagrams - the vocabulary queue designs still use today.
4.6 (581)Learning Domain-Driven Design
Vlad Khononov · 2021
Turns bounded contexts and aggregates into decision rules for where service boundaries actually go.
4.6 (400)