Ecommet
Topic 23 of 75

Service Discovery

Service instances start, stop and move all the time, so callers cannot hard-code addresses. Learn how a service registry, health checks and client-side or server-side discovery let services find each other.

Intermediate 6 min read 4 quiz questions

Jump to the quiz

In a nutshell

  • Instances come and go (deploys, autoscaling, crashes), so their addresses must be looked up, not hard-coded.
  • A service registry holds the live instances of each service; instances register and send heartbeats.
  • Client-side discovery: the caller asks the registry and picks an instance. Server-side: the caller hits a load balancer that does it.
  • Platforms such as Kubernetes give you discovery built in: a stable name and DNS entry per service.

The problem

The Checkout service needs to call Orders. Yesterday Orders ran on two servers; today autoscaling started a third, and a deploy replaced all three with new ones at new IP addresses. A config file with addresses in it is out of date within minutes.

Service discovery answers one question reliably: "where are the healthy instances of service X right now?"

The registry

A service registry is a small, highly available database of instances. Each instance registers itself (or is registered by the platform) when it starts, then sends a heartbeat every few seconds. If heartbeats stop, the entry expires and callers stop seeing it.

Client-side discovery

Client-side discovery Orders #1 → Registry: heartbeat Orders #2 → Registry Checkout → Registry: where is orders? Checkout → Orders #2: call 10.0.1.9 Registryorders: 2 liveCheckoutOrders #110.0.1.7Orders #210.0.1.9heartbeatwhere is orders?call 10.0.1.9

All steps Instances keep their entries alive with heartbeats. The caller looks up the list and picks one itself.

Diagram as text
  1. Each Orders instance registers on start-up and sends a heartbeat every few seconds.
  2. Checkout asks the registry for the live Orders instances (and caches the answer briefly).
  3. Checkout picks one, for example by round robin, and calls it directly.

A minimal registry with expiring entries looks like this:

A registry where entries expire unless heartbeats keep them alive

Java
import java.util.*;

class ServiceRegistry {
    private final double ttlSeconds;
    // service name -> (address -> time of last heartbeat)
    private final Map<String, Map<String, Double>> services = new HashMap<>();

    ServiceRegistry(double ttlSeconds) {
        this.ttlSeconds = ttlSeconds;
    }

    synchronized void heartbeat(String service, String address, double now) {
        services.computeIfAbsent(service, k -> new HashMap<>()).put(address, now);
    }

    synchronized void deregister(String service, String address) {
        Map<String, Double> live = services.get(service);
        if (live != null) {
            live.remove(address);
        }
    }

    synchronized List<String> instances(String service, double now) {
        Map<String, Double> live = services.getOrDefault(service, new HashMap<>());
        live.values().removeIf(last -> now - last > ttlSeconds);
        List<String> result = new ArrayList<>(live.keySet());
        Collections.sort(result);
        return result;
    }
}
Python
import threading

class ServiceRegistry:
    def __init__(self, ttl_seconds):
        self.ttl = ttl_seconds
        self.services = {}  # service name -> {address: time of last heartbeat}
        self.lock = threading.Lock()

    def heartbeat(self, service, address, now):
        with self.lock:
            self.services.setdefault(service, {})[address] = now

    def deregister(self, service, address):
        with self.lock:
            self.services.get(service, {}).pop(address, None)

    def instances(self, service, now):
        with self.lock:
            live = self.services.get(service, {})
            for address, last in list(live.items()):
                if now - last > self.ttl:
                    del live[address]
            return sorted(live)

Client-side vs server-side discovery

In server-side discovery the caller does not talk to the registry at all. It sends the request to a load balancer or proxy at a stable address, and that component looks up the instances and forwards the request.

Server-side discovery

Server-side discovery Checkout → orders.internal: call orders.internal → Orders #1 orders.internal → Orders #2 Checkoutorders.internalload balancerOrders #1Orders #2call
The caller only knows one stable name. The load balancer keeps track of the instances.
Diagram as text
  • Checkout → orders.internal: call
  • orders.internal → Orders #1
  • orders.internal → Orders #2
Client-sideServer-side
Who picks the instanceThe caller, using a libraryA load balancer or proxy
Extra network hopNoYes
Caller codeNeeds a discovery client in every language you usePlain HTTP to one name
Smart routingPossible per callerCentral, one place to configure
ExamplesNetflix Eureka with a client libraryKubernetes Services, cloud load balancers

DNS is the simplest form of discovery: a name resolves to the current addresses. It works everywhere, but clients and resolvers cache answers, so keep TTLs short and remember that DNS alone has no health information unless the DNS server checks health itself.

Health, not just presence

A process that is running is not always able to serve. Use two kinds of checks:

  • Liveness: is the process alive at all? If not, restart it.
  • Readiness: can it serve traffic right now (warmed up, connected to its database)? If not, keep it out of the registry or load balancer, but do not restart it.

On shutdown, an instance should deregister first, finish in-flight requests, then exit. Otherwise callers keep sending requests to an address that is about to disappear.

Common mistakes

  • Hard-coding IP addresses or host lists in config files.
  • Running a single registry node, which becomes a single point of failure for every call.
  • Treating "process is running" as "healthy"; use readiness checks.
  • Killing instances without deregistering and draining them first.

Interview questions

Try answering out loud before you open each one.

How do services find each other in your design?

Through service discovery. On Kubernetes, each service gets a stable DNS name and the platform routes to ready pods. Elsewhere, instances register with a replicated registry (Consul, etcd) and send heartbeats; callers look up instances and cache the result, or call a load balancer that does the lookup.

What happens if the registry goes down?

Calls should keep working on cached results for a while, which is why clients cache the last known instance list. The registry itself runs as a replicated cluster (usually 3 or 5 nodes with a consensus protocol), so losing one node does not lose it.

Check yourself

1. Why not keep service addresses in a config file?
Show the answer

Answer: Instances change all the time through deploys, scaling and failures. Addresses change constantly, so they must be looked up at runtime.

2. An instance stops sending heartbeats. What should the registry do?
Show the answer

Answer: Expire its entry after the TTL. Without heartbeats the entry expires, and callers stop being sent to it.

3. In server-side discovery, who chooses the instance?
Show the answer

Answer: A load balancer or proxy. The caller sends to one stable address; the load balancer picks an instance.

4. A service is running but still loading its cache. Which check should fail?
Show the answer

Answer: Readiness. It is alive (do not restart it) but not ready (do not send it traffic yet).

Further reading

Your notes

Saved in this browser as you type.

Finished this topic?

Mark it read to track your progress, then carry on with the next one.

Next topic