03 Β· System Design

Reliability & Observability

This is where you have the deepest lived experience relative to your level: you took an endpoint from 71 seconds to 2 seconds using distributed tracing, you run K8s deployments, and you keep a billing-grade pipeline alive. Most SDE-1/2 candidates know these topics from blog posts; you know them from pager reality.

The framing sentence. Open any reliability discussion with:

"Failures aren't exceptional in distributed systems β€” they're the steady state. So I design assuming every dependency will be slow or down sometimes, and the system's job is to degrade predictably instead of collapsing."

Everything on this page is a tool for that one goal.

Β§1SLIs, SLOs (and SLAs)

SLI

Indicator β€” a measurement. "p99 latency of POST /v1/chat", "% of 5xx responses", "Kafka consumer lag."

SLO

Objective β€” internal target on an SLI. "99.9% of requests succeed over 30 days"; "p99 < 500ms."

SLA

Agreement β€” the contractual, external version: SLO + penalties. Always looser than your SLO.

Error budgets β€” the idea that makes SLOs useful

99.9% over 30 days = 43.2 minutes of allowed badness per month. Budget remaining β†’ ship fast. Budget burned β†’ freeze features, fix reliability. It converts "be reliable" from vibes into an engineering currency that gates decisions.

Availability table worth memorizing:

99%    β†’ 7.3 h/month     99.9%  β†’ 43.8 min/month
99.95% β†’ 21.9 min/month  99.99% β†’ 4.4 min/month

Choosing SLIs (say this): measure what users experience, at the edge β€” request success rate and latency at the gateway, not CPU% on a pod. Use percentiles, never averages: an average hides that 1% of your requests take 30s. p99 matters because your busiest users hit it most often β€” a user making 100 calls has a 63% chance of hitting a p99 event.

Your hook: "With 50+ enterprise clients on the platform, latency and availability aren't abstract β€” enterprise contracts come with expectations. And LLM inference has fat-tailed latency by nature, so we track p95/p99 per route and per tenant, not averages."

Β§2Timeouts, Retries, Backoff + Jitter

Timeouts β€” the most underrated setting in distributed systems

No timeout = a hung dependency consumes your threads/connections until you are down too. Every network call gets a timeout, chosen from the dependency's actual p99 (e.g., p99 is 200ms β†’ timeout 500ms), not a default 30s.

Timeout budgets: if the gateway gives a request 2s total, the service must give its DB call less than that, minus its own work β€” timeouts should shrink as you go deeper in the call chain, or the outer caller gives up while inner work continues uselessly.

Retries β€” powerful and dangerous

Retry helps only for transient failures (network blip, brief overload, 503). Never retry non-idempotent operations without an idempotency key (see building blocks Β§13). Never retry 4xx β€” your request is wrong, and it won't get righter.

The retry storm: dependency slows β†’ everyone times out β†’ everyone retries β†’ 3Γ— load on an already-drowning service β†’ hard down. Retries convert partial failure into total failure unless bounded.

Exponential backoff + jitter

attempt 1: wait 100ms          Without jitter, 1000 clients that
attempt 2: wait 200ms          failed together retry TOGETHER β€”
attempt 3: wait 400ms          synchronized waves that re-kill the
attempt 4: wait 800ms  (cap ~10s)   recovering service.

FULL JITTER: sleep = random(0, min(cap, base Γ— 2^attempt))
β†’ spreads the wave into a smooth trickle.

Complete answer in one breath: "Bounded retries (2–3), exponential backoff with full jitter, only on idempotent operations, with a retry budget so retries can't exceed ~10–20% of traffic, and ideally only at ONE layer of the stack β€” if the gateway, the service, AND the client all retry 3Γ—, one failure becomes 27 attempts."

Retry Storm Visualizer

A flaky dependency is down for the first 3 seconds (shaded). Ten clients fire at tβ‰ˆ0. Watch where their attempts land β€” failed succeeded.

 Β·   Β· 

dependency down 0–3s recovered 3–10s

0s3s10s

Attempts hitting the struggling service: –  

Try it: retries 3, both toggles OFF β†’ synchronized bursts that all die inside the outage ("thundering herd"). Both ON β†’ attempts spread out and most requests succeed once the dependency recovers.

Your hook: "LLM providers rate-limit and hiccup routinely, so our gateway's retry/fallback logic is load-bearing β€” bounded retries with backoff, and provider fallback when a primary is degraded." (Bridges straight into circuit breakers.)

Β§3Circuit Breakers

Retries handle brief failures; circuit breakers handle sustained ones. Pattern: track a dependency's recent failure rate; past a threshold, stop calling it β€” fail fast instead of queueing doomed 30s timeouts.

Why fail-fast matters: waiting on a dead dependency holds threads, connections, and memory β€” the caller dies of resource exhaustion. That's how cascading failures propagate; the breaker is the firewall. On open circuit: serve a fallback (cache, default, queue-for-later), don't just error.

Circuit breaker state machine

CLOSED

Normal operation. Calls flow; failures are counted against a threshold.

OPEN

Calls fail fast β€” no network. After a cooldown (e.g. 30s), move to half-open.

HALF-OPEN

Let a few probe requests through. Succeed β†’ close. Fail β†’ back to open.

CLOSED Healthy. Failure rate below threshold (need 5 failures to trip).

Your hook: "In an LLM gateway this is concrete: if a provider's error rate spikes, we trip that provider open and route to a fallback provider β€” users see slightly different model behavior instead of timeouts. The breaker state itself is a metric worth alerting on."

Β§4Bulkheads

Named after ship compartments: partition resources so one flooding compartment doesn't sink the ship. Separate connection pools / thread pools / pod pools per dependency or per tenant class β€” slow dependency A exhausts its pool of 20 connections, while B and C sail on.

Multi-tenant angle (yours): one tenant's burst shouldn't degrade everyone. Per-tenant rate limits ARE a bulkhead at the front door; per-tenant concurrency caps at the service; optionally isolated capacity for whale tenants.

Your hook: "With 50+ enterprise clients, tenant isolation is the difference between one noisy tenant and one angry tenant β€” versus fifty."

Β§5Graceful Degradation

Design the answer to "what breaks first, and what do users see?" A degraded-but-usable system beats a down system:

  • Search ranking service down β†’ serve recency-sorted results.
  • Recommendations down β†’ serve popular items.
  • LLM provider down β†’ fall back to a secondary provider/model; if all down β†’ queue and notify, or return a clear, fast error.
  • Redis down β†’ fail open or fail closed? For rate limiting: usually fail open (allow traffic, log loudly) because blocking ALL paying customers over a limiter outage is the worse failure. Saying this tradeoff out loud is a strong signal.

Degradation must be designed and tested, not hoped for β€” name feature flags / kill switches as the mechanism.

Β§6Dead Letter Queues (DLQ)

In any queue consumer: some messages will never succeed (malformed payload, bug, poison pill). Infinite retry on a poison message halts the partition behind it β€” one bad message becomes an outage.

[Kafka topic] β†’ [consumer: try N times w/ backoff]
                     β”‚ still failing
                     v
               [DLQ topic] β†’ alert β†’ human/tooling inspects,
                              fixes, REPLAYS to main topic

Rules: attempts + error metadata travel with the DLQ'd message; DLQ depth is an alerting metric (nonzero = a bug you haven't met yet); build the replay path before you need it. Distinguish transient (retry in place) vs permanent (DLQ immediately) failures.

Your hook: "In our Kafka→OpenSearch pipeline, a malformed event must not stall the partition behind it — park it, alert, keep the pipeline draining, replay after the fix."

§7Distributed Tracing — and Your 71s→2s Story

Metrics say something is slow; logs say what one process did; traces show where the time went across services. A trace = one request's tree of spans (operation + start + duration + service), linked by a trace ID propagated through every hop (W3C traceparent header; OpenTelemetry is the standard toolkit). The gateway is the natural place to start the trace and inject the ID.

TRACE: POST /process        total: β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 71s
 β”œβ”€ gateway auth            β–Œ 40ms
 β”œβ”€ service A: handle       β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 70.8s
 β”‚   β”œβ”€ db query (x N!)     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 68s   ← THE CULPRIT
 β”‚   β”‚     └─ N+1: one query per item, serially
 β”‚   └─ downstream call     β–ˆ 2s
 └─ response serialize      β–Œ 100ms

Your story, told as a methodology β€” rehearse this ~90-second version:

"We had an endpoint taking 71 seconds. Instead of guessing, I went to the tracing data. Step one: get a full trace of a slow request and read the span waterfall β€” the question is always 'which span owns the time?' It wasn't spread evenly; the time concentrated in one service's data access. Step two: zoom into that span's children β€” the shape of the problem was visible in the trace structure: many small sequential operations instead of few batched ones. Step three: fix the access pattern β€” batch the work, eliminate the redundant round trips. Step four: verify with the SAME traces β€” the endpoint went to 2 seconds, a ~35Γ— improvement, and the waterfall now showed a healthy profile. The meta-lesson I took: never optimize on a hunch; make the system tell you where the time goes. Tracing turned a mystery into a 30-minute read."

Why interviewers love this: it demonstrates the loop β€” observe β†’ hypothesize β†’ fix β†’ verify with the same instrument. That's senior behavior at SDE-2 price.

Sampling (one-liner to volunteer): tracing every request is expensive at volume β†’ head sampling (random N%) vs tail sampling (keep the slow/error traces β€” the interesting ones).

Β§8Metrics, Logs, Traces β€” The Three Pillars

PillarNatureQuestion answeredCost profile
MetricsPre-aggregated numbers over time"Is something wrong?" (alerting)Cheap, fixed β€” but beware label cardinality
LogsDiscrete events, arbitrary detail"What exactly happened here?"Expensive at volume; structure them (JSON), index them
TracesPer-request causal tree"WHERE across services?"Sampled

Workflow to narrate: alert fires on a metric β†’ traces localize the offending service/span β†’ logs (jump via trace ID stamped in every log line) give the exact error. Correlation via a shared trace/request ID is what makes the three pillars one system.

Metrics vocabulary worth dropping: RED method for services (Rate, Errors, Duration) / USE for resources (Utilization, Saturation, Errors); Prometheus-style counters/gauges/histograms; percentiles from histograms.

Your hook: "Our observability doubles as a product: the same per-request metering that feeds dashboards also feeds billing β€” usage metering for 50+ clients is observability with an invoice attached, which means accuracy requirements are billing-grade, not best-effort. And log search itself is a system I run: logs/events flow through Kafka into OpenSearch β€” batched bulk ingestion β€” where 5,000+ concurrent users query them."

Β§9Health Checks

Liveness

"Is the process alive?" Fail β†’ K8s restarts the container. Keep it dumb (deadlock detection), or restarts amplify outages.

Readiness

"Can I serve traffic NOW?" Fail β†’ K8s removes the pod from Service endpoints, no restart. Checks deps: DB reachable, caches warm, config loaded.

Startup

Grace period for slow boots before liveness applies.

The classic mistake (name it): putting a hard dependency check in liveness β€” DB blips for 10s β†’ every pod restarts simultaneously β†’ thundering-herd reconnect β†’ worse outage. Dependency checks belong in readiness. Also: a deep readiness check that calls a shared dependency from 200 pods every 5s is itself load β€” keep probes cheap.

Your hook: "Every one of our 20+ services on K8s defines liveness and readiness probes β€” readiness gating is also what makes rolling deploys safe: the new pod takes traffic only when it proves it's ready."

Β§10Deployments: Rolling, Blue-Green, Canary

Rolling

(K8s default) Replace pods gradually (maxSurge/maxUnavailable), readiness-gated.

Pro: no extra infra, zero-downtime.
Con: two versions live simultaneously; slow full rollback.

Blue-green

Full parallel env; flip traffic at LB/gateway; keep old warm.

Pro: instant rollback (flip back); test green with prod parity.
Con: 2Γ— infra cost; DB schema shared by both β€” the real coupling.

Canary

1–5% of traffic β†’ new version; watch error rate/p99; ramp 5β†’25β†’100.

Pro: real-traffic validation, small blast radius.
Con: needs weighted routing (gateway!) + good metrics + patience.

Two things to always add:

  1. "Two versions run at once in every strategy β€” so DB migrations must be backward-compatible": expand β†’ migrate β†’ contract (add nullable column, dual-write/backfill, then drop old). Never a breaking migration in one deploy.
  2. Deploy β‰  release: feature flags decouple shipping code from exposing behavior β€” the cheapest rollback is a flag flip.

Your hook: "We do rolling deploys on K8s daily β€” readiness probes gate each step. And Kong is exactly where canary weighting lives: the gateway can split 95/5 by weight, which is the natural canary mechanism for our platform."

Β§11Backpressure

The problem: producer outruns consumer. Unbounded queues in between just delay and worsen the crash (memory bloat, then total loss). A system without backpressure doesn't degrade β€” it detonates.

Strategies, in escalation order:

  1. Bound every queue/pool β€” bounded queue + rejection beats unbounded queue + OOM.
  2. Slow the producer: TCP does this natively; app-level = blocking writes, semaphores, windowing (Kafka consumers pull at their own pace β€” pull-based consumption is built-in backpressure, a key reason the Kafka-in-the-middle architecture works).
  3. Shed load: return 429/503 + Retry-After early at the edge (cheap) rather than timing out late in the stack (expensive). Rate limiting IS pre-emptive load shedding β€” you built this.
  4. Degrade: sample, batch harder, drop low-priority work first (analytics before billing).
producer 10K/s ──> [bounded buffer] ──> consumer 5K/s
                        β”‚ full
                        β”œβ”€ block producer (backpressure)
                        β”œβ”€ reject new (shed load, 429)
                        └─ drop by priority (degrade)
   Kafka's answer: the LOG absorbs the burst durably;
   consumer lag grows and is VISIBLE; you scale consumers to drain.

Your hook: "Our pipeline handles this at two points: Kafka absorbs bursts durably so OpenSearch is never force-fed β€” consumers pull and bulk-index at the rate the cluster sustains, and lag is the pressure gauge. At the front door, per-tenant rate limits on Kong shed excess load before it costs us anything downstream."

Β§12Rapid-Fire Q&A Prep

"How do you know your service is healthy?"

SLIs at the edge: success rate, p95/p99 latency, plus consumer lag for pipelines. Alert on SLO burn rate, not on CPU.

"A downstream is slow. Walk me through what happens."

Timeout bounds the damage β†’ bounded retries w/ jitter β†’ breaker trips β†’ fallback/degrade β†’ bulkhead contains it β†’ alert fires β†’ trace localizes it. (This one answer tours the whole page.)

"How do you deploy without downtime?"

Rolling with readiness gates; canary via gateway weights for risky changes; expand-migrate-contract for schema; feature flags for instant rollback.

"Message keeps failing in your pipeline?"

Classify transient vs permanent; bounded retry with backoff; DLQ + alert; replay after fix; idempotent consumers make replay safe.

"Tell me about a hard performance bug."

The 71s→2s tracing story (§7). Have the 90-second version rehearsed: waterfall → culprit span → N+1 shape → batch the access pattern → verify with the same traces → ~35× improvement.

Β§13Self-Test

What's the difference between an SLI, an SLO, and an SLA?

SLI = the measurement (p99 latency, success rate). SLO = internal target on that SLI (99.9%/30d). SLA = the contractual external version with penalties, always looser than the SLO.

Your SLO is 99.9% over 30 days. How much downtime is that, and what's an error budget for?

~43 minutes/month. The budget is engineering currency: remaining budget β†’ ship features fast; burned budget β†’ freeze and fix reliability. It gates decisions instead of "be reliable" vibes.

Why must timeouts shrink deeper in the call chain?

Timeout budgets: if the gateway allots 2s total, inner calls must fit inside that minus local work β€” otherwise the outer caller gives up while inner work continues uselessly, burning resources for answers nobody is waiting on.

Recite the one-breath retry answer.

Bounded retries (2–3), exponential backoff with full jitter, only on idempotent operations, with a retry budget (~10–20% of traffic), and only at ONE layer of the stack β€” three layers each retrying 3Γ— turns one failure into 27 attempts.

Draw the circuit breaker state machine from memory.

CLOSED (counting failures) β†’ threshold crossed β†’ OPEN (fail fast, no network) β†’ cooldown elapses β†’ HALF-OPEN (few probes) β†’ probes succeed β†’ CLOSED; probes fail β†’ back to OPEN. On open: serve a fallback, don't just error.

Fail open or fail closed when Redis (your rate limiter) dies?

Usually fail open for rate limiting: allow traffic and log loudly, because blocking ALL paying customers over a limiter outage is the worse failure. Naming this tradeoff out loud is the signal.

Why does a DLQ exist, and what are the three rules?

Poison messages never succeed; infinite retry stalls the partition β€” one bad message becomes an outage. Rules: carry attempts + error metadata with the message; alert on DLQ depth (nonzero = unmet bug); build the replay path before you need it.

What goes in a liveness probe vs a readiness probe β€” and the classic mistake?

Liveness: dumb "process alive" checks only (fail = restart). Readiness: dependency checks β€” DB reachable, caches warm (fail = pulled from endpoints, no restart). Mistake: DB check in liveness β†’ a 10s DB blip restarts every pod at once β†’ thundering-herd reconnect.

Why must DB migrations be backward-compatible in every deploy strategy?

Two versions run at once in all of rolling/blue-green/canary. So: expand β†’ migrate β†’ contract (add nullable column, dual-write/backfill, drop old later). Never a breaking migration in one deploy. And deploy β‰  release β€” feature flags are the cheapest rollback.

Name the four backpressure strategies in escalation order.

1) Bound every queue/pool. 2) Slow the producer (TCP natively; Kafka pull-based consumption is built-in backpressure). 3) Shed load early at the edge (429/503 + Retry-After). 4) Degrade β€” drop low-priority work first (analytics before billing).

Tell the 71s→2s story in 90 seconds.

Trace the slow request β†’ read the span waterfall ("which span owns the time?") β†’ time concentrated in one service's data access β†’ children show many small sequential ops (N+1) β†’ batch the access pattern β†’ verify with the SAME traces β†’ 2s, ~35Γ—. Meta-lesson: never optimize on a hunch; make the system tell you where the time goes.

← Building blocks Classic designs β†’