03 Β· System Design

Your systems as interview answers

Three first-person talk-tracks that convert your production systems into system-design answers. Each has trigger questions, a script to rehearse aloud until it's ~5 minutes natural (not memorized-sounding), the reasoning made explicit, and the follow-ups to expect.

Ground rules. Every number below is a real fact from your work — 2M+ req/month, Kong on Kubernetes, 50+ enterprise clients, 5,000+ concurrent users, 20+ microservices, Kafka + batched OpenSearch ingestion, 71s→2s, PostgreSQL, Redis, Stripe/Razorpay. Never inflate them. Precise, modest numbers told with deep understanding beat big vague ones — interviewers probe, and probing is where you win.

T1"Design a billing-aware API platform"

Deploy when asked: design an API gateway Β· design a multi-tenant SaaS API Β· design usage-based billing Β· design a developer platform Β· "tell me about a system you've worked on" Β· design Stripe-for-X.

The script (first person)

"This is close to what I run in production, so let me describe how I'd design it β€” which is also how our system actually works.

The platform serves LLM APIs to 50+ enterprise clients at 2M+ requests a month. Three concerns have to be handled on every single request β€” who are you, are you allowed to make this call right now, and what should it cost you β€” and the worst design is scattering those across services. So the first architectural decision is a gateway layer: we run Kong on Kubernetes as the single front door, and behind it the platform is 20+ microservices that stay clean because auth, rate limiting, and metering live in one place.

Walking a request through: it hits Kong, which resolves the API key to a tenant β€” that lookup is cached in Redis, because adding a database read to every request would tax the whole platform. Then per-tenant rate limiting, which I built: limits are per-tenant and tiered, so the config is data, not code β€” an enterprise client's limits differ from a smaller one's, and changing them shouldn't need a deploy. The counters live in Redis so all gateway replicas share state β€” rate limiting on a single node's memory breaks the moment you scale horizontally.

Then the request routes to the right backend service. The services are stateless β€” all state is in PostgreSQL, Redis, or the event pipeline β€” which is what lets Kubernetes scale and reschedule them freely.

Now billing, which is the interesting part. The rule I design by: the billing record must be durable, but billing must never sit in the request path. If metering were synchronous, a billing-system slowdown would slow every API call β€” coupling revenue plumbing to user latency. So each request emits a usage event asynchronously; events flow through our Kafka pipeline and are aggregated into PostgreSQL as the usage ledger. Because the pipeline delivers at-least-once, the aggregation is idempotent β€” dedupe on event ID β€” so retries and replays can't double-bill anyone. That ledger drives invoicing, and we charge through Stripe and Razorpay β€” and working with payment APIs is where I really internalized idempotency: a timed-out charge call is ambiguous, so every mutation carries an idempotency key, and their webhooks are verified, acked fast, and processed idempotently because their retries will redeliver.

Failure thinking: the gateway is the obvious critical point, so it's N replicas β€” the platform has no single-box front door. If Redis degrades, rate limiting fails open β€” for a paid API, blocking all 50 tenants because the limiter's dependency blinked is the worse outage; we allow, log loudly, and alert. And billing events are the one thing we refuse to lose β€” durability there is the design's hard constraint."

Rehearse this track

Say it aloud from the structure, not word-for-word: gateway decision β†’ request walk-through β†’ billing rule β†’ failure thinking. When the timer goes amber, start landing the billing argument; when it goes red, get to fail-open and stop.

Why this works

  • Centralize cross-cutting concerns β†’ gateway pattern, justified from pain (50 tenants Γ— 20 services would otherwise mean 20 auth implementations).
  • Cache on the hot path, DB off it β†’ Redis for key lookup; latency budget thinking.
  • Config as data β†’ tiered limits without deploys; multi-tenant maturity signal.
  • Async + durable + idempotent = billing-grade β†’ the single strongest pattern statement in the script; it chains queue, at-least-once, idempotency into one causal argument.
  • Named failure policy (fail open) with a business justification β†’ this is the sentence that sounds like an SDE-2+, because it's a decision, not a feature.

Expected follow-up probes β€” answer aloud before peeking

"How exactly does the rate limiter work across nodes?"

Token bucket, Redis atomic Lua check-and-decrement, per-tenant keys (file 04b β€” full design ready).

"What if the event pipeline is down β€” do you drop billing events?"

No: local durable buffering + replay; degrade rather than drop; reconciliation job to catch drift.

"How would this change at 100x traffic?"

Gateway scales horizontally already; pressure lands on Redis (cluster/shard counters) and Postgres (partition ledger by tenant/month); the architecture holds β€” honest answer, shows you know where their ceilings are.

T2"Handle events from 5,000 concurrent users"

Deploy when asked: design an analytics/event pipeline Β· design log search Β· design activity feeds ingestion Β· "how would you ingest high-volume writes?" Β· design a metrics system Β· anything shaped many producers β†’ searchable store.

The script (first person)

"I run exactly this shape in production, so let me design it the way it actually works. The setup: 5,000+ concurrent users generating events across 20+ microservices, and those events need to end up searchable in OpenSearch.

The naive version is each service writing directly to OpenSearch as events happen. That fails in three distinct ways. Coupling β€” twenty services now each carry an OpenSearch dependency; if it's slow or down, twenty services feel it. Write pattern β€” thousands of individual index operations per second is the worst way to feed a search cluster; per-document indexing has fixed overhead, and the cluster spends itself on overhead instead of work. Burst behavior β€” user activity is spiky, and a synchronous design transfers every spike straight onto the search cluster.

So the design is: Kafka in the middle, batching at the sink.

Producers β€” the 20+ services β€” do one cheap, durable thing: append the event to Kafka and move on. Their latency and availability are now decoupled from the search cluster entirely; OpenSearch could be down for an hour and no user-facing service notices β€” events wait in the log.

On the other side, consumers read events in batches and use OpenSearch's bulk API β€” one bulk request carrying a large batch of documents instead of that many individual writes. The batching policy is size-or-time β€” flush when the batch is full or when a short timer fires β€” so under high traffic you get maximal efficiency and under low traffic events still land promptly rather than waiting for a batch to fill. That knob is a latency-versus-throughput dial, and it's tunable after deployment, which matters.

Correctness: Kafka gives at-least-once delivery β€” a consumer that crashes after indexing but before committing offsets will reprocess. So the writes are idempotent: deterministic document IDs make re-indexing an upsert β€” replay-safe by construction. And ordering: Kafka guarantees order only within a partition, so we partition by the key that users actually perceive order in β€” events for the same entity share a partition; global ordering is a cost nobody should pay for.

Operationally, the health metric for the whole pipeline is consumer lag β€” the gap between what's produced and what's consumed. Lag growing means consumers are falling behind: scale consumers up to the partition count, or tune batch sizes. And bursts get absorbed as visible, temporary lag rather than as a search-cluster meltdown β€” the log is the backpressure mechanism. A poison message β€” malformed event that always fails β€” must not stall the partition behind it, so after bounded retries it goes to a dead-letter topic with an alert, and we replay after fixing the bug; idempotent writes make replays safe."

Rehearse this track

Structure to hold: fail the naive design three ways β†’ Kafka in the middle β†’ batching policy β†’ at-least-once + idempotency + partitioning β†’ consumer lag and DLQ. Amber means move to correctness; red means land lag/DLQ and stop.

Why this works

  • Opens by failing the naive design in three named ways β€” showing you derive the architecture from the failure modes rather than pattern-matching "use Kafka."
  • Batching argued from first principles (fixed per-op overhead) with the size-or-time policy β€” the exact detail that proves you built this and didn't read it.
  • At-least-once + idempotent upserts and partition-key = perceived-order-domain β€” the two Kafka insights interviewers fish for.
  • Consumer lag as THE metric + DLQ β€” operator's-eye view; most candidates stop at the happy path.

Expected follow-up probes β€” answer aloud before peeking

"What batch size?"

Honest engineer's answer: it's a measured tradeoff β€” batch until either a size cap or a small time window; tune against indexing latency and cluster CPU; no magic constant.

"What if consumers can't keep up permanently?"

Scale to partition count; beyond that, more partitions (a planned migration), heavier batching, or sampling/dropping low-value events by class β€” degrade deliberately (analytics before anything billing-adjacent).

"Why Kafka and not a simple queue?"

Replay (reindexing OpenSearch from the log after a mapping change!), multiple independent consumer groups reading one stream, throughput, ordering by key. The replay point is especially strong β€” the log doubles as a rebuild source for the search index.

"Can you get exactly-once?"

Across an external sink, true exactly-once isn't practically what you engineer; at-least-once + idempotency gives exactly-once effect, cheaper and more robust.

T3"Debug a slow endpoint" — the 71s→2s story

Deploy when asked: "tell me about a hard bug" Β· "how do you approach performance problems?" Β· "walk me through debugging something in production" Β· behavioral rounds wanting a technical story Β· system-design rounds when observability comes up.

This is a methodology narrative, not a war story. The facts you own: an endpoint took 71 seconds, you used distributed tracing, it now takes 2 seconds β€” a ~35x improvement. Tell it as a repeatable process; the process is what they're hiring.

The script (first person)

"We had an endpoint in production taking 71 seconds. In a system of 20+ microservices, that's the worst kind of problem β€” the slowness could be anywhere in a chain of hops, and everyone's instinct suggests a different culprit. The lesson I took from this one is: don't debug by intuition; make the system show you where the time goes. So I went to distributed tracing.

Step one: get the shape of the problem. A trace of one slow request gives you the span waterfall β€” every hop and operation, each with its duration, as a tree. The first question is never 'what's wrong,' it's 'which span owns the time?' Seventy-one seconds spread thinly across many spans is a systemic problem; seventy-one seconds concentrated in one span is a local one. Reading the waterfall collapsed the search space from 'twenty services' to one place β€” that's the whole value of tracing: it turns a distributed mystery into a local question.

Step two: read the shape inside the culprit. Zooming into that span's children, the structure told the story β€” the time wasn't one expensive operation, it was an access pattern: many small sequential operations where the work should have been batched. Serial round trips stack linearly β€” do enough of them in a row and per-call overhead alone produces a colossal total. Nothing was 'broken'; the pattern was wrong. In my experience that's the most common production performance bug: not slow components, but chatty patterns between fast components.

Step three: fix the pattern, not the symptom. The fix was to restructure the access pattern β€” batch the work, eliminate redundant round trips β€” turning many serial calls into few. I deliberately did not start by adding caching or scaling hardware, because both would have masked the underlying pattern and left the real cost in place.

Step four: verify with the same instrument. Same endpoint, same tracing: 2 seconds β€” about a 35x improvement β€” and the new waterfall looked healthy, a short, flat profile instead of a long staircase. Measuring with the same tool that found the problem closes the loop; 'it feels faster' isn't verification.

What it changed about how I work: I now treat traces as the first move on any latency issue, not the last resort β€” and it's why, when I design systems, trace propagation goes in on day one, at the gateway. When this endpoint misbehaved, the instrumentation determined whether diagnosis took thirty minutes or a week."

Rehearse this track

Hold the arc: observe β†’ localize β†’ hypothesize from structure β†’ fix the cause β†’ verify with the same instrument. Amber means you should be at step three; red means deliver "2 seconds, ~35x" and the closing moral.

Why this works

  • The narrative arc is observe β†’ localize β†’ hypothesize from structure β†’ fix the cause β†’ verify with the same instrument β€” a transferable method, told with a memorable moral ("chatty patterns between fast components", "long staircase β†’ short flat profile").
  • It quietly demonstrates systems maturity: search-space reduction, refusing to cache over a pathology, verification discipline, and instrumentation-as-design-principle.
  • 71β†’2 is a spectacular but real number; delivering it inside a calm methodology makes it credible instead of boastful.

Expected follow-up probes β€” answer aloud before peeking

"Why not just add a cache?"

Caching hides the pattern and adds invalidation complexity for a problem that had a structural fix; cache after the access pattern is sane, if still needed.

"How do you catch this class of issue before production?"

p95/p99 dashboards per route with alerting on regression, trace sampling that keeps slow-request traces (tail sampling), latency budgets per hop.

"What was the actual fix?"

Keep it at the pattern level honestly: restructured serial per-item round trips into batched operations; the principle is that per-call overhead Γ— N serial calls was the cost, and batching removed the N.

⚑Random probe drill

One probe, ten seconds, out loud

Deals one random follow-up from all three tracks' probe pools. You get a 10-second think-countdown β€” answer aloud before the reveal appears.

Β§Using these in the room

1 Β· Bridge lines

Earn the pivot, don't hijack: "Can I ground this in a system I actually run? I think it maps one-to-one." Interviewers nearly always say yes, and the answer instantly upgrades from theory to testimony.

2 Β· One number per sentence, max

"Kong on Kubernetes, 50+ enterprise clients, 2M+ requests a month" lands; a stat barrage sounds rehearsed.

3 Β· Let them interrupt

These scripts are trees, not monologues β€” every paragraph is a branch they may probe, and each probe is a chance to go deeper than the script. Files 02–05 are your depth behind each branch.

4 Β· Same stories, two registers

System-design rounds: lead with architecture, reference yourself for credibility. Behavioral rounds: lead with situation and your actions, keep architecture as supporting detail. Same facts, different emphasis.

← LLM-system designsAPI design β†’