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."
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."
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."
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
Β§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.