Build Your Own Load Balancer — a design session, not a reading session

This is not "learn how load balancers work" — you already know that. This is: sit down and design one, piece by piece — functional requirements, components, low-level APIs, data model, DB sizing, algorithm internals, failure handling, scaling — with a thought-provoking question at every step. Attempt your own answer on paper or out loud before revealing. The reveal is a reference design, not "the" answer — your version can and should differ, as long as you can defend it.

10 segments ~2.5–3 hrs, self-paced Format: design it yourself → reveal → compare Goal: produce your own artifact, not memorize mine
step 01

Functional & non-functional requirements — before any design exists

Every real design review starts here, and most rushed ones skip it. Before you draw a single box, you need to be able to say precisely what this load balancer must do, and under what numbers it must keep doing it.

Build it yourself first: On paper, write your own functional requirements list (5–7 bullets) and non-functional targets (throughput, latency, availability) for a load balancer sitting in front of a mutual-fund order-service pool. Then reveal.
Q1What are the functional requirements — what must this load balancer actually be able to do? Try to list at least 6 before revealing.reveal
A reasonable functional set:
  • Accept incoming client connections/requests and forward to a backend
  • Maintain a live pool of backend servers (add/remove dynamically)
  • Health-check backends and exclude unhealthy ones from selection
  • Select a backend using a configurable algorithm (round robin / least-connections / weighted)
  • Support graceful draining of a backend before removal
  • Expose a control-plane API to register/deregister backends and update config
  • Expose metrics (selection counts, error counts, latency) for observability
Notice none of this is "which algorithm" — that's a single line item inside a much bigger functional surface.
Q2What non-functional targets actually shape the design — not just "make it fast," but specific numbers you'd defend in a review?reveal
For a mid-sized FinTech order-flow pool, reasonable targets to state explicitly:
  • Throughput: sustain 20k–50k rps per LB node at steady state, burst to 100k rps on event days (IPO/allotment)
  • Added latency: LB overhead itself should stay under 2–3ms p99 (it's a hop, not a bottleneck)
  • Availability: LB layer itself target 99.99% — it can't be less available than the backends it protects
  • Backend pool churn: new backend visible to routing within 5–10 seconds of registration
  • Health detection: unhealthy backend removed within 2–3 failed checks (few seconds), not minutes
Writing these down forces every later decision (algorithm, data structure, health-check interval) to be justified against a number, not a vibe.
Q3Why does "consistency vs. availability" show up even in something as "simple" as a load balancer's backend list?reveal
Because the backend pool is shared, frequently-changing state. If every LB node must have a perfectly consistent view before routing a request, you've made the LB layer depend on coordination — exactly the bottleneck it's meant to prevent downstream. The realistic answer: favor availability with eventual consistency for the backend list (a few seconds of staleness is acceptable), while backend health status should lean more real-time since it directly affects correctness of routing decisions.
Q4What should you explicitly declare as out of scope for this build, and why does stating that matter as much as the in-scope list?reveal
Reasonable out-of-scope declarations: full WAF/security filtering, API-gateway-style auth/transformation, multi-region global traffic management, and TLS certificate management complexity. Stating this matters because an interviewer or reviewer — and your own design — needs a boundary; without it, "build a load balancer" silently expands into "build a gateway, a CDN, and a global traffic manager," and none of those get designed properly because none of them got scoped in the first place.
step 02

Scope and level — what exactly are you building?

This is the decision from the concepts document, but now you have to actually commit to one for your build, and justify it against the requirements you just wrote.

Build it yourself first: Decide — is your load balancer L4, L7, or both layered? Write one paragraph justifying the choice against your throughput and functional requirements above.
Q5Given the functional requirement "select a backend using host/path awareness eventually," does that force you to build L7 from day one?reveal
Not necessarily. A defensible design: build the core as L7 (HTTP-aware) since the functional requirements already assume request-level decisions (health checks over HTTP, per-request algorithm selection), but keep the listener/proxy layer modular enough that a pure L4 passthrough mode could be added later for non-HTTP traffic. Committing to L7 now is reasonable because the stated functional requirements already need request-level visibility — the requirements should drive the level choice, not the other way around.
Q6Should your load balancer terminate TLS itself, or pass it through? Decide, and state the one consequence you're accepting.reveal
Reasonable design: terminate TLS at the LB. Consequence accepted: the LB becomes part of the security trust boundary — it now holds certificates/keys, and everything downstream trusts headers/identity the LB asserts, so backend network access must be locked down to LB-only (see the security segment in the main notes) or that trust becomes forgeable.
Q7Will your design support sticky sessions from day one, or defer it? Justify against your functional requirements list.reveal
Defer it, but design the selector interface so it can be added without a rewrite — e.g. the backend-selection function should accept an optional routing key (cookie/session id) from day one, even if the first shipped algorithm ignores it. This is a good example of scoping: you're not building stickiness now, but you're not designing yourself into a corner that makes it expensive later either.
step 03

Component breakdown — what are the actual moving pieces?

Now the real design work starts: decompose the load balancer into components with clear, single responsibilities. This is the piece most self-taught designs get wrong — treating "load balancer" as one monolithic blob instead of a small system of cooperating parts.

Build it yourself first: Draw your own component diagram — boxes and arrows — before revealing. Aim for 5–7 components. Ask yourself: what does each one own, and what does it explicitly NOT own?
                          +-------------------+
                          |   Control Plane    |  <- register/deregister backends,
                          |   API (mgmt)        |     update weights, trigger drain
                          +---------+---------+
                                    |
                                    v
   Client                 +-------------------+        +------------------+
   requests -----------> |     Listener /     |<------>|  Backend Registry |
   (data plane)           |   Request Handler   |        |  (in-memory view) |
                          +---------+---------+        +------------------+
                                    |                              ^
                                    v                              |
                          +-------------------+        +------------------+
                          |  Selector / LB     |------->|  Health Checker   |
                          |  Algorithm Engine   |        |  (background loop) |
                          +---------+---------+        +------------------+
                                    |
                                    v
                          +-------------------+
                          |   Backend Pool     |
                          |   (actual servers)  |
                          +-------------------+

Cutting across all of the above: Metrics/Observability Exporter, Config Store (durable)
Q8Why should the Health Checker be a separate component from the Selector, rather than the Selector just checking health inline on every request?reveal
Because checking health inline on the request's hot path would add latency to every request and couple request throughput to health-check cost. Health checking needs to run on its own background cadence (e.g., every 2–5 seconds), independently, and simply update a shared "current health view" that the Selector reads from — a cheap, non-blocking lookup, not a live check per request.
Q9Why does the Backend Registry need to be a distinct component from the Control Plane API, instead of the API directly holding the state?reveal
Separation of concerns: the Control Plane API is the write path (accepts registration/deregistration/config changes, validates them), while the Registry is the read-optimized in-memory structure the Selector consults on every request. If they were the same component, every high-frequency read (selection) would compete with low-frequency but potentially slow writes (registration, validation, persistence) for the same lock/resource.
Q10What does the "Config Store (durable)" box actually need to persist, and why can't everything just live in memory?reveal
In-memory state (current health status, live connection counts) is fine to lose and rebuild on restart — it's transient by nature. But configuration — the backend list itself, weights, algorithm choice, routing rules — must survive an LB node restart or it silently reverts to defaults after every deploy/crash. That's the durable/config vs. live/derived split every component boundary in this diagram is quietly built around.
Q11If you had to merge two of these components into one to reduce operational complexity, which two would you merge — and what would you lose?reveal
A defensible merge: Health Checker and Backend Registry, since the health checker's only job is updating the registry's health field — running them as one process removes an IPC/network hop. What you lose: you can no longer scale or restart health-checking independently of the registry (e.g., temporarily disabling health checks for a maintenance window without touching registry state becomes harder). This is the kind of trade-off worth being able to state out loud in a review, even if you don't ultimately choose it.
step 04

Low-level API design — the control plane, precisely

"Build the APIs" is not a checkbox — it's where vague components become concrete contracts. Design the control-plane API surface: who calls it, with what payload, and what it guarantees.

Build it yourself first: Write out 5 API endpoints (method + path + one-line purpose) for your control plane before revealing. Think about who the caller is for each one — a deploying service? a human operator? an autoscaler?
POST /v1/backends
Register a new backend. Caller: the service's own deploy pipeline or self-registration agent. Body: host, port, initial weight, pool/service id. Returns: backend_id. Does NOT immediately receive traffic — starts in a PENDING state until the first successful health check.
DELETE /v1/backends/{backend_id}
Deregister a backend. Caller: deploy pipeline on scale-down. Behavior: does not immediately remove — transitions the backend to DRAINING first (see segment 8), actual removal happens once in-flight connections complete or a drain timeout elapses.
PATCH /v1/backends/{backend_id}/weight
Update a backend's routing weight. Caller: an operator doing a canary rollout, or an autoscaler adjusting for instance size differences. Body: new integer weight.
POST /v1/backends/{backend_id}/drain
Explicitly trigger draining without deregistering. Caller: deploy tooling, ahead of a rolling restart. Lets a backend be pulled from new-traffic eligibility while investigation happens, without losing its registration.
GET /v1/pools/{pool_id}/backends
List current backend pool with live health/weight/state. Caller: observability dashboards, on-call engineers debugging routing.
PUT /v1/pools/{pool_id}/policy
Update the pool's selection algorithm and health-check policy (interval, thresholds, deep vs shallow). Caller: a platform/SRE-owned config change, not per-service self-service — this one has wider blast radius.
Q12Why does POST /v1/backends put a new backend into a PENDING state instead of ACTIVE immediately?reveal
Because registration only proves the backend exists in the registry — it says nothing about whether it's actually ready to serve (segment 3 in the main notes: discovery vs. readiness are different questions). Routing to it immediately risks sending real traffic to a backend that's still warming up or mid-deploy. PENDING → ACTIVE only after N consecutive successful health checks is the correct default.
Q13Why is DELETE not immediate — why route it through DRAINING instead of removing the backend from the registry right away?reveal
Immediate removal would cut off in-flight requests exactly like the ungraceful-deploy scenario from the leadership section. Making DELETE trigger a drain-then-remove sequence by default means callers get safe behavior even if they didn't explicitly think to call the drain endpoint first — the safe path is the default path, not an opt-in.
Q14Why does PUT /v1/pools/{pool_id}/policy deserve a different authorization model (platform/SRE-owned) than POST /v1/backends (self-service)?reveal
Because registering your own backend affects only your service's slice of traffic, but changing the pool-wide algorithm or health-check thresholds affects every backend and every caller in that pool at once — the blast radius is fundamentally different. APIs should be scoped for authorization based on blast radius, not just on "does this team own the underlying service."
Q15What idempotency property should POST /v1/backends have, and why does it matter for a control-plane API specifically?reveal
Registering the same host:port twice should not create two entries — the API should treat (pool_id, host, port) as a natural key and return the existing backend_id on a duplicate call, rather than erroring or duplicating. This matters because deploy pipelines retry on failure by default; a non-idempotent registration API will eventually double-register a backend after a flaky network call, corrupting the pool.
step 05

Data model & DB sizing — what state actually needs to persist, and how much?

Important distinction: you are not sizing for request traffic (the LB doesn't store requests). You're sizing for control-plane / config metadata — the backend registry, health history, and policy config. This is usually a small, low-write dataset — knowing that, and being able to say why, is itself the signal of good judgment here.

Build it yourself first: Sketch your own schema (2–3 tables) for backends, pools, and health status. Then estimate: how many rows, how many writes/sec, and how much storage for a platform with 80+ services, ~10 backends per service on average?
TABLE backends
  backend_id (PK), pool_id (FK), host, port, weight, state (PENDING/ACTIVE/DRAINING/REMOVED),
  registered_at, last_health_check_at, consecutive_failures, consecutive_successes

TABLE pools
  pool_id (PK), name, algorithm (round_robin/least_conn/weighted), health_check_policy_id, created_at

TABLE health_check_policy
  policy_id (PK), interval_ms, timeout_ms, failure_threshold, success_threshold, check_type (tcp/http/deep)

-- NOT stored here: per-request logs, request bodies, traffic data (that's the metrics/observability pipeline's job)
Q16Estimate row counts: 80+ microservices, ~10 backend instances each on average, one pool per service. How many rows in `backends` and `pools`?reveal
pools: ~80–100 rows (one per service/pool). backends: ~800–1,000 active rows (80 services × ~10 instances), plus some churn overhead if you keep recently-REMOVED rows for audit (say 3x that for a rolling 30-day audit window = ~3,000 rows). This is a trivially small dataset by database standards — a few thousand rows, not millions. The sizing exercise itself teaches the lesson: the LB's own metadata store is tiny; it's the traffic it routes that's large, and that traffic is never persisted here.
Q17Estimate write throughput to this store. Is it closer to 10 writes/sec, 1,000 writes/sec, or 100,000 writes/sec — and why?reveal
Closer to single digits to low tens of writes/sec platform-wide, and even that's bursty (deploys, autoscaling events), not sustained. Backend registration/deregistration happens on deploy cadence (maybe a few times per minute across 80 services during business hours), not per-request. This is the key sizing insight: the control-plane data store has request-independent, low write volume — it should never be designed as if it needs to scale with traffic RPS, because health-status updates (the actual frequent writes) are usually kept in-memory/cache, not written straight to the durable DB on every check.
Q18Given writes are this infrequent and the dataset this small, does this justify a distributed database, or is something simpler defensible?reveal
A single well-replicated relational DB (e.g., Postgres with a couple of read replicas), or even an embedded config store synced via a control-plane service, is entirely defensible at this scale — reaching for a distributed database here would be solving a scale problem you don't have. The interesting design decision isn't the DB technology; it's making sure each LB node caches this data in-memory and only re-syncs periodically, so the data store's availability doesn't become a single point of failure on the data path (segment 9 territory).
Q19Where should live health-check results (updated every few seconds per backend) actually be stored — the durable DB, or somewhere else? Why?reveal
Live health results should stay in each LB node's local in-memory state (possibly shared via a fast cache like Redis if multiple LB nodes need a consistent view), not written to the durable config DB on every check. Writing health pings to a relational DB every 2–5 seconds per backend, across 800+ backends, would generate meaningful sustained write load for information that's inherently transient and doesn't need durability — if an LB node restarts, it just re-runs health checks and rebuilds the view in seconds.
Q20What's the actual takeaway about DB sizing for a load balancer, stated as one sentence you could say in an interview?reveal
"The load balancer's own persistent storage needs are small and low-write — because it's a routing and control-plane system, not a system of record for traffic — and the interesting sizing decision isn't the database's scale, it's correctly separating durable config (small, low-write, needs a real DB) from live/derived state (health, connection counts — in-memory, ephemeral, rebuilt on restart)."
step 06

Algorithm internals — pick one, and design it precisely

"Weighted least connections" as a phrase is not a design. Here's where you actually specify the data structure and the exact steps the Selector runs on every request.

Build it yourself first: Pick weighted least-connections. Before revealing, write pseudocode for: (a) the data structure holding backend state, (b) the selection function run per request, (c) what updates on request-start and request-end.
struct BackendState {
  backend_id, weight, active_connections, health_status
}

// In-memory, per LB node:
backend_pool: Map<backend_id, BackendState>   // updated by Health Checker + connection tracking

function select_backend(pool):
  healthy = [b for b in pool if b.health_status == ACTIVE]
  if healthy is empty: return OVERLOAD_OR_503
  // score = active_connections / weight  (lower score = more headroom relative to capacity)
  best = min(healthy, key = lambda b: b.active_connections / b.weight)
  return best

on_request_start(backend):
  backend.active_connections += 1

on_request_end(backend):
  backend.active_connections -= 1
Q21Why divide active_connections by weight instead of just comparing raw active_connections across backends?reveal
Because raw connection counts alone assume every backend has equal capacity. If S1 has weight 5 (bigger instance) and S2 has weight 1, S1 should fairly hold ~5x the connections of S2 before either becomes the better next choice. Dividing by weight normalizes "connections" into "connections per unit of declared capacity," so the comparison is fair across heterogeneous backends — this is exactly the weighted least-connections algorithm sketched in the concepts document, made concrete.
Q22This selection function runs on every single request. What data structure property does it need to be safe under concurrent access from many request threads at once?reveal
It needs thread-safe, low-contention updates — active_connections is being incremented and decremented by many concurrent requests simultaneously. A naive global lock around the whole map would serialize every request through one lock, defeating the purpose. The realistic answer: use atomic counters per backend (lock-free increment/decrement) so only the rare full pool-membership change (backend added/removed) needs a heavier lock, not every single request.
Q23If `healthy` is empty (every backend failed health checks simultaneously), what should `select_backend` actually do — crash, return an error, or something else?reveal
Return a controlled failure — a 503 with a clear "no healthy backends" signal — never crash the LB process itself. This ties directly to overload protection (main notes segment 10): the LB should have an explicit code path for "the whole pool is down," not treat it as an unhandled edge case that surfaces as a null-pointer exception or hang.
Q24Why should on_request_end always run, even when the request fails or the client disconnects early?reveal
Because if a failed or aborted request never decrements active_connections, that backend's counter leaks upward permanently — over time, every backend looks artificially "busier" than it really is, and the algorithm's fairness silently degrades. This is a classic resource-accounting bug: on_request_end belongs in a finally/cleanup path, not just the success path.
step 07

Health checker — design it as its own subsystem

You already know health checks can lie (main notes segment 5). Now design the actual subsystem: what runs, on what schedule, and how it talks to the rest of your components.

Build it yourself first: Design the health checker's loop — what does it do every tick, what config does it need per backend, and how does a state transition (ACTIVE → UNHEALTHY) actually get applied?
every interval_ms (per pool's health_check_policy):
  for backend in pool.backends:
    result = run_check(backend, check_type, timeout_ms)   // TCP / HTTP / deep
    if result == success:
      backend.consecutive_successes += 1
      backend.consecutive_failures = 0
      if backend.state == UNHEALTHY and consecutive_successes >= success_threshold:
        backend.state = ACTIVE   // re-admit
    else:
      backend.consecutive_failures += 1
      backend.consecutive_successes = 0
      if backend.state == ACTIVE and consecutive_failures >= failure_threshold:
        backend.state = UNHEALTHY   // remove from routing eligibility
Q25Why use consecutive_failures/successes with thresholds, instead of flipping state on the very first failed or first successful check?reveal
This is the flapping-prevention mechanism from the concepts document, made concrete. Flipping on one data point makes the pool membership hostage to a single transient network blip. Requiring N consecutive results before changing state trades a little detection latency for a lot of routing stability — the exact threshold values (e.g. 3 failures / 2 successes) should be tuned per pool based on how expensive a false removal vs. a missed detection is for that specific service.
Q26Should the health checker run as part of each LB node, or as a separate, single centralized service?reveal
Running it per LB node is more resilient (no single point of failure for health detection) but means N LB nodes are each independently hitting every backend — multiplying load on backends by N. A centralized health-check service avoids that multiplication but becomes its own single point of failure and adds a hop between "check result" and "LB's routing decision." A common middle ground: a small dedicated health-checking tier (2–3 instances, not 1, not N) that all LB nodes subscribe to for results.
Q27For the payment/order-service pool specifically, what check_type would you choose, and what's the one risk you're deliberately accepting?reveal
A reasonable choice: a lightweight HTTP check that exercises the critical dependency path but is cached/rate-limited on the backend side (e.g., checks DB connectivity but reads a value cached for 1–2 seconds rather than issuing a fresh query every poll) — deeper than a pure TCP check, cheaper than an uncached deep check. Risk accepted: the cached value could theoretically mask a DB failure that happens within that 1–2 second cache window — a deliberate, bounded trade-off, not an unnoticed gap.
step 08

Failure & draining logic — design the state machine, not just the happy path

You now have PENDING, ACTIVE, UNHEALTHY, DRAINING, REMOVED scattered across earlier segments. Pull them into one explicit state machine — this is what an interviewer or a code reviewer will actually ask you to draw.

Build it yourself first: Draw the full backend lifecycle state machine — every state, and the exact trigger for every transition — before revealing.
        register()                 N consecutive
           |                        successful checks
           v                              |
       PENDING  ------------------------> ACTIVE  <---------------+
                                            |   \                  |
                          N consecutive     |    \  drain()        | N consecutive
                          failed checks     |     \  or DELETE     | successful checks
                                            v      v                |
                                        UNHEALTHY  DRAINING ---------+
                                            |          |
                                  drain()   |          | in-flight conns = 0
                                  or DELETE |          | OR drain_timeout elapsed
                                            v          v
                                         REMOVED <-- REMOVED
Q28Can a backend go directly from UNHEALTHY back to ACTIVE, or must it always pass through some intermediate check?reveal
It should require the same N-consecutive-successes threshold as PENDING → ACTIVE — a backend recovering from UNHEALTHY hasn't proven it's stable yet, just that one check passed. Treating "recovered" the same as "newly registered and proven" (both need the same success streak before re-admission) keeps the state machine consistent and prevents a flapping backend from being re-admitted on a single lucky check.
Q29What should happen if a backend is UNHEALTHY and someone calls drain() on it — does draining even mean anything for a backend that's already excluded from routing?reveal
It still matters — an UNHEALTHY backend is excluded from new routing decisions, but if it still holds long-lived connections from before it went unhealthy (a WebSocket, a streaming session), drain() gives those a bounded time to complete before the backend is force-removed, rather than dropping them the instant health status flips. UNHEALTHY and DRAINING are answering different questions — "can it take new work" vs. "should its old work be cut off" — so both states can coexist meaningfully.
Q30What happens if a DRAINING backend never reaches zero in-flight connections — a stuck long-lived connection that just never closes?reveal
This is exactly why the state machine needs a drain_timeout as an alternate exit condition, not just "in-flight connections = 0." Without a timeout, a single misbehaving client could keep a backend stuck in DRAINING (and therefore stuck consuming capacity/holding infra) indefinitely. The timeout forces eventual REMOVED even if it means force-closing that one stuck connection — a deliberate, bounded trade-off between graceful and guaranteed.
Q31Where does retry logic plug into this state machine — should the LB itself retry a failed request on a different backend, and if so, under what condition?reveal
Retry should be a property configured per pool/route, not global (echoing the leadership scenario about platform-wide retry causing duplicate redemptions). A defensible default: retry is allowed only for requests explicitly marked idempotent (e.g., via an idempotency-key header the caller supplies), and only for a narrow set of failure types (connection refused, timeout before any response byte received) — never after a response has started streaming back, since that implies the backend already began processing.
step 09

Scaling your own load balancer layer

Your LB is now a real system with its own state. The final design question: how does it scale out without becoming the very bottleneck it exists to prevent?

Build it yourself first: If one LB node isn't enough, how do you run N of them? What do they share, what do they keep local, and how do they stay roughly in sync? Write your answer before revealing.
        Clients
           |
           v
  +-----------------------------+
  |  Entry (VIP / DNS / ECMP)    |   <- segment 2 of the concepts doc: LB's own availability problem
  +-----------------------------+
     |        |        |        |
     v        v        v        v
   LB1       LB2      LB3      LB4     <- each: own in-memory copy of registry + health view
     \        |        |        /
      \       |        |       /
       +------+--------+------+
              |
              v
       Config Store (durable, low-write)   <- source of truth, polled/pushed periodically
       Health results: local per node OR shared cache (Redis) if strict consistency needed
Q32Should LB1 and LB2 coordinate to agree on exactly which backend to pick for every request, or is it fine if they make slightly different choices?reveal
Fine — even desirable — for them to differ slightly. Forcing agreement means a synchronous coordination step on every request, turning the LB layer itself into a serialization point. Each node keeping its own in-memory view (refreshed every few seconds from the config store, and its own locally-run health checks) tolerates small windows of inconsistency in exchange for each node being fully independent and fast on the hot path — the same trade-off named explicitly in the concepts document's scaling segment.
Q33A new backend is registered via the control-plane API. How long, realistically, before every LB node is routing to it — and is that delay a problem?reveal
If each LB node polls the config store every 5 seconds, worst case is just under one poll interval per node, so realistically within 5–10 seconds all nodes converge. Given the NFR target set in segment 1 ("new backend visible within 5–10 seconds"), this is acceptable — the design decision here is making sure that number was chosen deliberately against a stated requirement, not left as whatever the default polling interval happened to be.
Q34What's the failure mode if the Config Store itself goes down for 10 minutes? Does the whole LB layer go down with it?reveal
No — and designing for this is the whole point of each LB node caching its own copy. If the config store is unreachable, each LB node should keep serving traffic using its last-known-good in-memory state, simply logging/alerting that it can't refresh, rather than failing closed. The config store's availability should degrade the platform's ability to change routing, not its ability to continue routing — that distinction is exactly what separates a resilient design from a fragile one.
Q35How would you know, with actual numbers, when it's time to add another LB node — what metric would trigger that decision?reveal
Tie it back to the NFR from segment 1: if per-node throughput approaches the 20k–50k rps steady-state target, or if p99 added-latency creeps toward the 2–3ms ceiling under current load, that's the trigger — not a vague "it feels slow." Designing the non-functional requirements up front is exactly what turns "should we scale out" from a guess into a measurable, defensible decision.
step 10

End-to-end assembly — put your own design in one diagram

Last step. No new concepts — just synthesis. This is what you'd draw on a whiteboard in the last five minutes of an interview to prove the pieces actually cohere into one system.

Build it yourself, one more time: Without looking back at the earlier segments, draw the full request path AND the full control-plane path in one diagram, labeling every component from segment 3. Then compare against the reveal below.
DATA PLANE (per request):
Client --TLS--> Listener --> Selector (reads in-memory Registry) --> chosen Backend
                                  ^                        |
                                  |                (on_request_start / on_request_end
                          Health Checker              update active_connections)
                        (background loop,
                         updates Registry's
                         health_status)

CONTROL PLANE (config changes):
Operator/Pipeline --> Control-Plane API --> validate --> write --> Config Store (durable)
                                                                        |
                                                            periodic poll/push
                                                                        v
                                                        each LB node's local Registry copy

FAILURE PATHS baked in:
- No healthy backend -> 503 (never crash)
- Backend exceeds failure_threshold -> UNHEALTHY (excluded from Selector)
- DELETE/scale-down -> DRAINING -> REMOVED (bounded by drain_timeout)
- Config Store unreachable -> LB nodes keep serving on last-known-good cache
Q36Looking at your own diagram: which single component, if it silently failed, would be hardest to detect — and does your design have a way to notice?reveal
Usually the Health Checker itself — if it silently stops running (crashed goroutine, stuck deadlock), every backend keeps whatever health state it last had, potentially forever, with no error raised anywhere. The design needs a meta-check: the health checker's own liveness (e.g., "last successful check cycle completed at time T") should itself be an exported metric with alerting, or a broken health checker becomes invisible until backends that actually died are still being routed to.
Q37If you had to cut this design down to an MVP buildable in two weeks, which three components would you keep, and which would you explicitly defer?reveal
A defensible MVP: Listener + Selector (simple weighted round robin, no dynamic weight updates yet) + Health Checker (shallow HTTP only). Deferred: the full Control-Plane API (start with static config file instead of live registration endpoints), DRAINING state machine (accept brief connection drops on removal initially), and the shared config store (single LB node only, no multi-node sync yet). Being able to name the MVP cut — and explain what risk each deferral accepts — is itself a senior-level signal.
Q38Final check: does your design actually satisfy the non-functional requirements you wrote in segment 1? Go back and verify each one against a specific component.reveal
This is deliberately the one question without a "reveal" answer written for you — the exercise only works if you trace it yourself: throughput target → which component could bottleneck it (usually the Selector's lock contention, addressed in segment 6); availability target → does the design tolerate Config Store and Health Checker failures gracefully (segment 9); backend-visibility latency → is the poll interval actually tuned to the number you committed to. A design that was never checked against its own stated requirements isn't finished — it's just drawn.