Vibe Engines
YouTube
System Design

Design a Service Discovery System

Step 1 / 9

Learn system design by building a service discovery system like Consul, etcd, Eureka or Kubernetes DNS step by step.

The numbers to beaton startregisteron stopderegisterautomatictied to lifecycle

The whole design, in writing

Learn system design by building a service discovery system like Consul, etcd, Eureka or Kubernetes DNS step by step. An interactive guide covering why hardcoded IPs break in a dynamic fleet, a service registry with self-registration, health checks and heartbeats, client-side vs server-side discovery, backing the registry with consensus for HA, caching and watches, and the unhappy paths (stale entries, AP-vs-CP, split brain).

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is service discovery?

In a microservices fleet, "payments" isn't one server — it's a shifting set of instances that autoscale up and down, get redeployed, move hosts, and die. When "orders" needs to call "payments", which IP does it use? Hardcode one and it's wrong within minutes. The addresses are never stable.

Consumerneeds service B
New in this step: Consumer.

Service discovery lets services find each other by logical name instead of hardcoded address. Instances register their location in a registry; consumers look up the current healthy instances of a service and call one. Health checks keep the directory honest, and the registry itself is made highly available.

What the new pieces do

Consumerclient
A service that must call another. It knows the logical name ("payments") but not which instances are alive or where.

Step 1 · The hardcoded trap

Static IPs break

The simplest approach: put the target service's IP in a config file. It works on day one. Why does it fall apart in a real, dynamic fleet?

Consumerneeds service B
The system as it stands at this step.

Hardcoded target IPs in config. Why does it break in a microservices fleet?

  1. In a dynamic fleet, instances are ephemeral: autoscaling adds/removes them, deploys replace them, crashes kill them, schedulers move them. A hardcoded IP points at something that's often gone, and it can't spread load across the live set. Addresses must be looked up, not fixed.

  2. Editing isn't the problem; the churn is. Even a perfectly-edited IP is stale minutes later when that instance is replaced or rescheduled. You need dynamic lookup, not better editing.

  3. Storage is trivial. The real issue is that the mapping from service name to live instances changes constantly, so any static snapshot is wrong almost immediately.

Instances are ephemeral: autoscaling, deploys, crashes and rescheduling add and remove them constantly, so any hardcoded address is stale within minutes and can't spread load across the live set. In a dynamic fleet, the mapping from service name → live instances changes continuously. You must look it up, not bake it in.

Why this piece earns its place

The tempting cheaper answer is to keep one fixed address per service and point it at a load balancer — a virtual IP, or a single hostname the consumer never has to change. It genuinely does remove the churn from the consumer’s config, which is why it survived so long in static estates, and it is worth being able to argue against properly rather than dismissing it. Look at what sits behind that address. The load balancer still has to know which instances are alive right now, and maintaining that membership list is precisely the problem we just said no human process can keep up with. You have not removed discovery, you have relocated it into a box someone else configures — and if the list behind it is still hand-edited, autoscaling and rolling deploys invalidate it exactly as fast as they invalidated the config file. Done properly, with a registry behind it, this becomes the server-side option four steps from now. The lesson is narrower than addresses are bad: a stable name is fine, but a stable name backed by a stale membership list is the original bug wearing a hostname.

Step 2 · A live directory

The service registry

If addresses must be looked up, something has to hold the current map of name → live instances. What is it, and how do entries get in?

Registryname → instances
New in this step: Registry.

Introduce a service registry: a live directory mapping each service name to its currently-registered instance addresses. Instances put themselves in it (next step), and consumers query it to get the current set. The registry is the single source of truth for "who's out there right now" — the heart of discovery.

Why this piece earns its place

This step quietly assumes a name maps to one flat set of interchangeable instances, and that assumption holds for about a week. Then you are running two versions side by side through a rolling deploy, or a canary that should take a thin slice of traffic, or instances spread across zones where a consumer would much rather call the ones beside it, or a staging fleet that must never be reachable from production. A directory that stores only name and address cannot express any of that, so teams express it in the only field they have — the name. You get payments-v2, payments-canary, payments-euw1, every consumer hardcodes which variant it wants, and changing where traffic goes is a config edit and a redeploy again. So make an entry richer than an address from the very first version: every registration carries tags — version, zone, environment, weight — and a lookup filters on them. The name stays the stable thing consumers depend on, and everything that actually varies lives inside the entry, where it can be changed without touching the caller.

What the new pieces do

Registrygateway
The live directory: instances register here; consumers query it for the current healthy addresses of a service.

Step 3 · Getting in the book

Self-registration

How do the registry's entries appear and stay current as instances start and stop? Who's responsible for registering an instance?

Registryname → instancesInstance Apayments:9001Instance Bpayments:9002Instance Cpayments:9003
New in this step: Instance A, Instance B, Instance C.

How do instances get into (and out of) the registry as they start/stop?

  1. Manual registration can't keep up with autoscaling and deploys happening every few seconds. Registration must be automatic.

  2. Consumers can't know what's running — that's the entire problem discovery solves. Instances themselves (or a platform agent) must report their existence.

  3. On boot, an instance registers its address with the registry; on graceful shutdown it deregisters. In self-registration the instance does this directly; in third-party registration a platform (like Kubernetes) registers pods for you. Either way it's automatic and tied to the instance lifecycle.

Instances self-register: on startup an instance tells the registry "payments is available at 10.0.0.7:9001", and on graceful shutdown it deregisters. (In the third-party variant, the platform — e.g. Kubernetes — registers/deregisters instances for you as pods come and go.) Registration is automatic and tied to the instance lifecycle, so the directory tracks the fleet without humans.

Why this piece earns its place

Self-registration answers the question is the process up, and consumers need the answer to a different one: can this instance serve a request. A process that has just started has empty connection pools, cold caches and unwarmed code paths; if it registers the moment it boots, the registry starts handing its address out seconds before it can do anything useful, and every rolling deploy produces a burst of timeouts nobody can reproduce afterwards. So gate registration behind the same readiness check the instance would expose to a probe, and only register once it passes. The second trap is subtler: what address does the instance report? An instance knows its own hostname, which behind NAT, inside a container, or on a multi-homed host is frequently not something any consumer can dial — it registers a value that is perfectly true and completely unreachable, and the entry looks healthy because nothing about it is malformed. Either have the platform supply the routable address, which is one of the strongest real arguments for third-party registration, or make the first successful check from outside the instance the thing that promotes an entry to usable.

  • on startregister
  • on stopderegister
  • automatictied to lifecycle

What the new pieces do

Instance Aservice
One running instance of the target service. It self-registers its address on startup and heartbeats while alive.
Instance Bservice
Another instance. Autoscaling and deploys add and remove these constantly — their addresses are never stable.
Instance Cservice
A third instance. The registry must always reflect exactly which of these are currently healthy.

Step 4 · Only the living

Health checks & heartbeats

Graceful deregistration is the happy path — but instances also crash without a chance to deregister. If the registry keeps handing out a dead instance's address, consumers fail their calls. How does the registry know an instance is actually alive?

Instance Bpayments:9002Instance Cpayments:9003Health Checksheartbeat · TTL
New in this step: Health Checks.

An instance crashes without deregistering. How does the registry stop handing out its address?

  1. Either the registry actively probes each instance's health endpoint, or instances send periodic heartbeats with a TTL. An instance that fails checks or stops heartbeating is marked unhealthy and removed, so consumers only ever get live addresses.

  2. Relying on consumers to notice failures is slow and pushes many failed calls before removal. The registry must proactively track liveness via checks/heartbeats.

  3. Crashes skip deregistration, so "registered" and "alive" diverge constantly. The registry must continuously verify liveness, not trust the last registration.

Track liveness continuously. Either the registry actively probes each instance's health endpoint, or instances send periodic heartbeats with a TTL — miss a few and you're presumed dead. An instance that fails checks (or stops heartbeating) is deregistered, so consumers only ever receive addresses of instances that are healthy right now. Crash cleanup is automatic.

Why this piece earns its place

The knob this introduces is how long an instance may stay silent before you presume it dead — the heartbeat interval multiplied by the misses you tolerate. Set it long and a crashed instance keeps collecting traffic for that whole window, which is the failure the step exists to prevent. Set it short and you start evicting the living: a stop-the-world garbage collection pause, a slow disk, one dropped packet window, and a perfectly healthy process is deregistered, vanishes from the fleet, re-registers, and flaps. The way to set it is to measure your own pause and latency distribution and sit above its tail, then accept the remaining window as the price of not flapping. The direction that hurts most is the correlated one. If the registry loses its network rather than the instances losing theirs, every heartbeat stops at once, and a literal-minded TTL deregisters the entire fleet — a checker problem turned into a total outage. So cap it: when an implausibly large share of instances go quiet in the same moment, stop expiring and keep serving the last known set, because the far likelier explanation is that the thing doing the counting is wrong.

  • heartbeat / probeprove liveness
  • TTLmiss → presumed dead
  • deregistercrash cleanup

What the new pieces do

Health Checksservice
Probes/heartbeats that tell the registry which instances are alive; dead ones are deregistered so no one calls them.

Step 5 · Who does the lookup?

Client-side vs server-side discovery

A consumer needs one healthy instance to call. Should the consumer query the registry and pick an instance itself, or should a router in the middle do it? This choice shapes where the discovery logic lives.

ConsumerRegistryInstance AInstance BInstance CRouter / LB
New in this step: Router / LB. · swipe to pan the diagram

Where should the "query registry + pick an instance" logic live?

  1. Client-side discovery is one valid option (fast, one less hop), but it puts discovery + load-balancing logic in every service and every language. The alternative — a router/proxy — keeps clients dumb. Both are legitimate; it's a tradeoff, not "always client".

  2. Server-side discovery (a router/LB queries the registry) is valid and keeps clients simple, but adds a network hop and a component to run. It's not the only answer — client-side avoids the hop. It's a tradeoff.

  3. In client-side discovery the consumer asks the registry and load-balances across instances itself — fewer hops, but logic in every client. In server-side discovery a router/proxy (or platform LB) queries the registry and forwards — dumb clients, but an extra hop and component. Pick per ecosystem; service meshes push this into a sidecar.

Two patterns. Client-side discovery: the consumer queries the registry, gets the instance list, and load-balances itself — fewer hops, but discovery logic lives in every client/language. Server-side discovery: a router/proxy (or platform load balancer) queries the registry and forwards the call — clients stay dumb, at the cost of an extra hop and a component to run. Both are valid; modern service meshes push this logic into a per-instance sidecar.

Why this piece earns its place

The extra hop is the wrong thing to decide this on — one more hop inside a datacenter is rarely what hurts. The cost that actually decides it is the upgrade path. Client-side discovery ships the registry client, the balancing policy and the retry behaviour inside every application binary, so the day you change how instances are weighted, or fix a bug in that library, the rollout is a redeploy of every service that depends on it, coordinated across teams who get nothing out of your change. Introduce a second language and you either reimplement all of it or run two implementations that quietly disagree about what healthy means. Server-side puts that logic in one place you can upgrade on your own schedule, and the bill is honest: the router now sits on the data path of every call, needs its own capacity and its own availability story, and when it fails everything fails together — whereas client-side degrades one consumer at a time. The sidecar is attractive on exactly this axis: the logic leaves application code, but it is deployed per instance, so upgrading it is a fleet operation instead of a pull request in thirty repositories.

  • client-sideconsumer picks, 1 fewer hop
  • server-siderouter picks, dumb client
  • sidecarmesh factors it out

What the new pieces do

Router / LBservice
The server-side option: a proxy that consults the registry and load-balances, so consumers stay dumb.

Step 6 · The registry can't lie or die

Consensus & availability

The registry is now critical infrastructure that everything depends on. If it goes down, no one can discover anyone; if two copies of it disagree during a partition, services get contradictory routing. How do you make it both available and consistent?

RegistryHA · consensusConsensus Storeetcd / Raft
New in this step: Consensus Store.

Back the registry with a consensus system — etcd, Consul, or ZooKeeper (Raft/ZAB) — so a majority quorum agrees on the registry state and it survives node failure without losing data or splitting. This is a deliberate CP choice (consistent, may reject writes during a partition). The alternative, Eureka, chooses AP: stay available and serve possibly-stale data during partitions, betting that slightly-stale routing beats no routing. Pick per your tolerance — and either way, let consumers cache so a registry blip doesn't stop calls.

Why this piece earns its place

Consensus is the right home for the registry’s state and the wrong place to put the fleet’s churn. Every write through Raft or ZAB is a majority round trip that has to be durably logged before it counts, so if each instance’s heartbeat is a write, the registry’s load grows with the size of the fleet multiplied by the heartbeat rate — and the traffic costing the most carries the least information, namely still alive, nothing changed. That is how a quorum store gets tipped over by an ordinary deploy rather than by an outage. Keep liveness cheap and separate: a heartbeat lands on the node the instance is connected to, and only a genuine membership change — a registration, a deregistration, an instance crossing its threshold — is committed through consensus. The other thing a quorum quietly assumes is that the majority is nearby. Stretch one quorum across regions and every write pays a wide-area round trip, while a regional split leaves the minority side unable to write at all. Run a quorum per region and federate between them, so a region that loses its neighbours can still discover itself.

  • quorummajority agrees
  • CPconsistent (etcd/Consul/ZK)
  • APavailable, stale (Eureka)

What the new pieces do

Consensus Storedata
The registry's replicated, consistent backing store, so the directory survives node failure without losing or splitting.

Step 7 · Fast and fresh

Caching, watches & DNS

Consumers can't query the registry on every single call — that's a lookup per request and huge load on the registry. But cached instance lists go stale as instances change. How do you get both speed and freshness?

ConsumerRegistryConsensus StoreInstance AInstance BInstance CHealth ChecksRouter / LB
The system as it stands at this step. · swipe to pan the diagram

Consumers cache the instance list locally (fast lookups, resilience if the registry blips) and stay fresh via watches/subscriptions — the registry pushes changes so caches update within seconds instead of polling. Expose discovery through familiar interfaces: many systems offer DNS-based discovery (resolve payments.svc to current instances) alongside a rich API. Kubernetes bakes this in — a Service name resolves via cluster DNS to healthy pod endpoints, with kube-proxy/the mesh load-balancing.

Why this piece earns its place

A watch inverts the freshness problem in a way that is easy to miss. With polling, a broken registry shows up as an error on every attempt. With a watch, it shows up as silence — and silence is indistinguishable from nothing has changed. A consumer whose subscription died an hour ago has a cache that looks perfectly healthy and a picture of the fleet that is an hour old, and it will go on calmly routing to instances that were retired long ago. So the subscription needs a cursor: the revision the client has already seen, sent on reconnect, so the registry either replays what was missed or explicitly tells the client the gap is too old and it must re-list. Being told is the important half — a client that silently resumes from a revision the registry has already compacted has lost changes without ever knowing. Then make cache age a first-class signal rather than an internal detail: every consumer should know, and be able to report, how long it has been since its view was last confirmed. Serving from a stale cache through a registry outage is the behaviour we want; not knowing the cache is stale is not.

  • cacheno lookup per call
  • watchpush updates in seconds
  • DNStransparent resolution

Step 8 · The sharp edges

Stale entries, herds & split brain

Discovery has classic failure modes: an instance dies but lingers in caches (stale entry), a registry restart triggers a re-registration stampede, and a network partition can split the registry's view.

ConsumerRegistryConsensus StoreInstance AInstance BInstance CHealth ChecksRouter / LB
The system as it stands at this step. · swipe to pan the diagram

Tolerate stale entries at the caller with retries, timeouts and circuit breakers — assume any address might be dead and fail over to another instance. Smooth heartbeat/registration storms with jitter and sensible TTLs so a registry restart doesn't get hammered. Handle partitions per your CP/AP choice (quorum rejects a minority side; AP serves stale). And keep discovery decoupled from the call path via caching so the registry is never a synchronous dependency of every request.

Why this piece earns its place

The catch in tolerate stale entries at the caller is that retries are not free, and you need them most at the moment they cost most. A dead address in a cached list is not a rare event during a deploy — it is a steady share of every consumer’s calls, and if each of those calls quietly attempts a couple of alternates, a partly unhealthy fleet has its load multiplied onto the instances that are still standing, which is how one bad instance becomes an outage. So give retries a budget: cap them as a fraction of a consumer’s outbound traffic rather than as a fixed count per call, so a broadly failing dependency makes retrying stop rather than accelerate. Scope the breaker to the instance, not the service — a ghost address should be ejected from that one consumer’s local list while its siblings keep taking traffic, whereas a service-level breaker punishes healthy instances for a dead neighbour. And only replay what is safe to replay: a timeout does not tell you the instance never received the request, and blindly retrying a non-idempotent call on a second instance is how a stale-address annoyance becomes a data problem.

You did it

You just designed service discovery.

ConsumerRegistryConsensus StoreInstance AInstance BInstance CHealth ChecksRouter / LB
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Hardcoded IPs break because instances constantly autoscale, deploy, crash and move.
  • A service registry maps logical names to current live instance addresses — the phone book.
  • Instances self-register on start and deregister on stop (or a platform registers them).
  • Health checks and heartbeats deregister crashed instances so only live addresses are handed out.
  • Client-side vs server-side discovery decides where lookup + load-balancing lives (sidecars split the difference).
  • Back the registry with consensus (CP: etcd/Consul/ZK) or accept AP staleness (Eureka) — plus client caching.
  • Cache + watch for fast, fresh lookups; DNS-based discovery (as in Kubernetes) makes it transparent.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why do hardcoded addresses fail in a microservices fleet when they were fine in a static setup?

    Because the fundamental assumption changed: instances are now ephemeral. In a static, hand-provisioned setup a service lived at a known IP that essentially never changed, so baking it into config was fine. In a modern fleet, autoscaling spins instances up and down in response to load, rolling deploys continuously replace old instances with new ones at new addresses, crashes remove instances without warning, and schedulers (like Kubernetes) move workloads between hosts — so the set of live instances for a service, and their addresses, changes on the order of seconds. Any hardcoded address is therefore stale almost immediately and points at something that may be gone, and it also can't spread load across the current set or route around a dead instance. The mapping from a service's logical name to its live instances is now a fast-changing, runtime fact, which means consumers must resolve it dynamically at call time rather than depend on a fixed snapshot — the same reason DNS exists, but updated in seconds.

  2. Why does the registry need health checks in addition to registration and deregistration?

    Because graceful deregistration only covers the happy path. When an instance shuts down cleanly it can tell the registry "remove me," but instances also crash, hang, lose network, or get killed abruptly — and in those cases they never get the chance to deregister, so their entry lingers in the registry even though the process is dead. If the registry kept handing that address to consumers, every consumer would attempt calls to a ghost and fail. Health checks close this gap by continuously verifying liveness rather than trusting the last registration: either the registry actively probes each instance's health endpoint on an interval, or instances must send periodic heartbeats carrying a TTL and are presumed dead if they miss several. An instance that fails its checks or stops heartbeating is automatically deregistered, so the directory reflects who is actually alive right now, not merely who once registered. This is the same discipline a load balancer uses to evict dead backends, and it's what makes "the registry says it's there" trustworthy.

  3. Client-side vs server-side discovery — what's the tradeoff?

    They differ in where the "query the registry and choose an instance" logic lives. In client-side discovery, the consumer itself asks the registry for the list of healthy instances of the target service and load-balances across them directly, then calls the chosen instance. This is efficient — there's no extra network hop — and gives the client full control over balancing, but it pushes discovery and load-balancing logic into every service and every language you use, coupling your application code to the registry and duplicating that logic everywhere. In server-side discovery, the consumer simply calls a stable endpoint fronted by a router or load balancer (or a platform LB), and that component queries the registry and forwards the request to a live instance. This keeps clients dumb and centralizes routing policy, but adds a network hop and another component you must run and scale. The modern compromise is the service mesh: a per-instance sidecar proxy handles discovery and balancing transparently, giving client-side's low latency and locality while factoring the logic out of application code into infrastructure. Which you pick depends on your ecosystem — Kubernetes leans server-side/DNS with kube-proxy, while meshes like Istio/Linkerd use sidecars.

  4. Should the registry be CP or AP, and why does it matter?

    It's a direct application of the CAP tradeoff to the directory itself, and the right answer depends on how much stale routing data hurts you versus how much a registry outage hurts you. A CP registry (etcd, Consul, ZooKeeper, backed by Raft or ZAB) always returns consistent data agreed by a majority quorum and never splits its view, but during a network partition the minority side may refuse writes (and possibly reads), so registration/updates can be temporarily unavailable — you trade availability for never serving contradictory or divergent state. An AP registry (Eureka is the classic example) instead stays available on all sides of a partition and may serve slightly stale instance lists, betting that momentarily out-of-date routing is far less harmful than being unable to discover anything. For discovery specifically, slightly-stale data is often tolerable because a consumer that gets a dead instance simply retries another one (which is why you always pair discovery with client-side caching, retries, and circuit breakers) — so many production systems lean AP for resilience, while systems that use the same store for leader election or config that must be strongly consistent choose CP. The key point is that the registry is critical infrastructure everything depends on, so you must make this availability-vs-consistency choice deliberately rather than by accident.

  5. Why can't consumers just query the registry on every call, and how do caching and watches fix it?

    Querying the registry on every single outbound call would make the registry a synchronous dependency of every request in the entire system and generate a lookup per call — enormous load that turns the registry into both a bottleneck and a single point of failure whose latency and outages would directly stall all inter-service traffic. The fix is to have consumers cache the resolved instance list locally so the vast majority of calls do a fast in-memory lookup with no registry round trip, which also means a brief registry hiccup doesn't stop calls (the cache still has healthy addresses). The danger of caching is staleness, and that's addressed with watches (subscriptions): instead of polling, the consumer subscribes to changes for the services it uses, and the registry pushes updates when instances register or deregister, so caches converge to the new reality within seconds while adding almost no steady-state lookup load. Many systems also expose discovery via DNS so it's transparent to applications (resolve a service name to current endpoints), though DNS's own caching adds lag, which is why change-sensitive systems prefer explicit watches. The net effect is speed and freshness together, with the registry kept off the hot path of every request.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Hardcoded service addresses break because…

    The name → live-instances mapping changes continuously; resolve it at call time.

  2. Instances get into the registry via…

    Automatic, lifecycle-tied registration keeps the directory current without humans.

  3. Health checks/heartbeats exist because…

    Registered ≠ alive; probe/heartbeat and deregister the dead so only live addresses are served.

  4. Client-side vs server-side discovery is about…

    Client-side: fewer hops, logic everywhere. Server-side: dumb clients, extra hop. Sidecars split it.

  5. The registry stays trustworthy and available by…

    etcd/Consul/ZK give consistency; Eureka favors availability; caching keeps calls flowing.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Look up by name: resolve a logical service name (“payments”) to a current healthy instance to call.
  • Register: instances self-register their address on start and deregister on graceful stop.
  • Health-check: probes/heartbeats deregister crashed instances so only live addresses are handed out.
  • Discover: client-side (consumer queries + balances) or server-side (a router/proxy does it).
  • Stay fresh: consumers cache the instance list and watches push changes within seconds.

The qualities that shape everything

Each one names the mechanism that buys it.

Resolve names, not stale addresses
A service registry holds the live name → instances map; consumers look it up at call time instead of baking in an IP that’s stale within minutes.
Track the fleet without humans
Instances self-register on startup and deregister on shutdown (or a platform agent does it) — registration is automatic and tied to the instance lifecycle.
Never route to a dead instance
Health checks / heartbeats with a TTL continuously verify liveness; an instance that fails is deregistered, so consumers only get addresses healthy right now.
Put discovery logic in the right place
Choose client-side discovery (consumer queries + load-balances) or server-side (a router/proxy does it), with service meshes pushing it into a sidecar.
The registry can’t die or split-brain
Back it with consensus (etcd/Consul/ZK, a majority quorum) for CP, or accept Eureka-style AP staleness — plus client caching so a blip doesn’t stop calls.
Fast lookups that stay fresh
Consumers cache the instance list locally and subscribe to watches so the registry pushes changes in seconds — no lookup per call.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Dynamic name → instance lookup over a hardcoded IP in config

In a dynamic fleet instances autoscale, deploy, crash and move, so any fixed address is stale within minutes and can’t spread load across the live set. Resolve the name at call time instead of baking it in.

Automatic self-registration over an admin adding each instance by hand

Manual registration can’t keep up with autoscaling and deploys happening every few seconds. Tie registration to the instance lifecycle so the directory tracks the fleet without humans.

Health checks + heartbeats over trusting that registered means alive

Crashes skip deregistration, so registered and alive diverge constantly — without liveness verification the registry hands out ghost addresses. Probe/heartbeat with a TTL and deregister the dead.

Client-side vs server-side per ecosystem over forcing all discovery logic into every client

Client-side avoids a hop but re-implements balancing in every service and language; server-side keeps clients dumb at the cost of a hop and a component. It’s a real tradeoff — meshes push it into a sidecar.

AP + client caching (or CP consensus) over a single-node registry

The registry is critical infrastructure everything depends on — CP (etcd/Consul/ZK quorum) stays consistent but may reject writes in a partition; AP (Eureka) stays available but serves stale entries. Discovery tolerates slight staleness, so many lean AP + caching.

The answer, out loud

What a strong answer to “Design a Service Discovery System” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    What we’re actually building

    Let me start by naming the problem. We have a fleet of services, and one of them needs to call another. I want a caller to be able to say the name — payments — and get back an address that is alive right now. So the functional requirements I’d write down are: resolve a name, register an instance, health-check it so dead ones disappear, and keep consumers fresh as the fleet changes underneath them. The one non-functional thing I care about more than the rest is that discovery must not become a synchronous dependency of every request, because then everything I build sits on top of it staying up.

  2. 4–9 min

    Why we can’t just write the address down

    The naive version is an address in a config file, and it’s worth saying why it fails, because that tells us what the design has to do. Instances are ephemeral now. Autoscaling adds and removes them, rolling deploys replace them at new addresses, crashes take them out with no notice, and a scheduler can move them between hosts. So the set of live instances for a service is a runtime fact that changes continuously, not a deployment-time fact you can write down once. Once I accept that, the shape of the answer is forced: consumers depend on a stable logical name and resolve it to a current address at call time.

    Built in step 1: Static IPs break
  3. 9–14 min

    The registry

    Something has to hold that mapping, so I’d draw a registry: a directory from a service name to the addresses currently registered for it. A phone book that gets rewritten constantly. Everything after this is one of two questions — how the directory stays accurate, and how it stays available. I’d keep the data model deliberately boring, because the hard part here is never the lookup, it’s the churn going in and the failure behaviour around it.

    Built in step 2: The service registry
  4. 14–20 min

    How entries get in

    Getting entries in has to be automatic, because the churn is faster than any human process. An instance registers itself when it starts and deregisters on a graceful shutdown. The alternative is third-party registration, where the platform watches instances and maintains their entries for them — Kubernetes does exactly that with pods. I’d choose based on how much I want living in application code. Self-registration is a handful of lines but couples every service to the registry’s client; third-party keeps the application ignorant that discovery exists, at the cost of more infrastructure to run.

    Built in step 3: Self-registration
  5. 20–26 min

    Registered isn’t alive

    Graceful deregistration only covers the polite exits, and the interesting ones are never polite. A process that crashes, hangs, or loses its network never gets to say goodbye, so its entry stays and the registry keeps offering an address for something that’s gone. That’s why liveness has to be verified rather than assumed. Either the registry probes each instance, or instances heartbeat against a lease and are presumed dead once they stop. At fleet scale I lean towards heartbeats, because the work scales with the instances doing it rather than piling a fan-out problem onto the registry.

    Built in step 4: Health checks & heartbeats
  6. 26–33 min

    Where the lookup happens

    Then, who actually does the lookup. In client-side discovery the consumer asks the registry, gets the list, and picks an instance itself. In server-side discovery it calls a router or a platform load balancer, and that does the asking and the picking. I wouldn’t pretend one is correct — they put the same logic in different places. In a single-language shop I might start client-side. In a polyglot estate I’d go server-side, or straight to a service mesh, where a sidecar beside each instance handles discovery and balancing and the application just makes an ordinary call to a name.

    Built in step 5: Client-side vs server-side discovery
  7. 33–38 min

    The registry can’t die

    By now everything depends on the registry, so it has to be more available than anything using it. I’d back it with a consensus system — etcd, Consul or ZooKeeper — so a majority quorum agrees on the state and it survives losing nodes without splitting. That’s a CP choice: consistent, but it can refuse writes during a partition. Eureka takes the opposite bet and stays available while serving possibly stale entries. For discovery I think the availability argument is strong, because a slightly stale list is survivable — you call a dead instance, you retry another — whereas being unable to discover anything at all is not.

    Built in step 6: Consensus & availability
  8. 38–43 min

    Fast, fresh, off the hot path

    The last structural piece is that consumers must not call the registry on every request. They cache the instance list and keep it fresh with a watch, so the registry pushes changes rather than being polled. That buys two things at once: lookups become local and cheap, and a registry outage doesn’t stop traffic, because every consumer still holds a recent list and keeps calling the instances that are up. Plenty of platforms wrap all this in DNS so applications never learn discovery exists, which is lovely for adoption and costs some freshness to DNS caching.

    Built in step 7: Caching, watches & DNS
  9. 43–45 min

    The trade-off, and what’s next

    So the trade-off at the centre: this design is deliberately eventually consistent. Every consumer acts on a view of the fleet that is slightly old, and I chose that, because the alternative — a strongly consistent lookup on the call path — makes the registry a hard dependency of every request in the system. The price is that callers have to assume any address might already be dead. With more time I’d work through how discovery meets deploys, so an instance is drained out of consumers’ lists before it stops rather than as it stops, and I’d cover who is allowed to register, because anything that can put itself in the directory can attract traffic.

What this teaches

Learn system design by building a service discovery system like Consul, etcd, Eureka or Kubernetes DNS step by step. An interactive guide covering why hardcoded IPs break in a dynamic fleet, a service registry with self-registration, health checks and heartbeats, client-side vs server-side discovery, backing the registry with consensus for HA, caching and watches, and the unhappy paths (stale entries, AP-vs-CP, split brain).

Key takeaways

  • Hardcoded IPs break because instances constantly autoscale, deploy, crash and move.
  • A service registry maps logical names to current live instance addresses — the phone book.
  • Instances self-register on start and deregister on stop (or a platform registers them).
  • Health checks and heartbeats deregister crashed instances so only live addresses are handed out.
  • Client-side vs server-side discovery decides where lookup + load-balancing lives (sidecars split the difference).
  • Back the registry with consensus (CP: etcd/Consul/ZK) or accept AP staleness (Eureka) — plus client caching.
  • Cache + watch for fast, fresh lookups; DNS-based discovery (as in Kubernetes) makes it transparent.

Concepts covered

  • What is service discovery?
  • Static IPs break
  • The service registry
  • Self-registration
  • Health checks & heartbeats
  • Client-side vs server-side discovery
  • Consensus & availability
  • Caching, watches & DNS
  • Stale entries, herds & split brain
built so services find each other while instances come and go by the second — make the calls, kill the registry, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs