R8 · Part 2 · The technical rounds
System design
At six years nobody expects you to design Twitter for 500 million users. They are checking whether you can scope a problem, choose a data model, and say what you are giving up. A design with no stated trade-off is not a design — it is a diagram.
- product
- saas
- service (less often)
- agency (less often)
Everything open, top to bottom.
10On this page
- Design a camp or event booking marketplace.
- Design a real-time dashboard for AI agent performance metrics.
- Design an e-commerce listing page that survives a traffic spike.
- Design a notification service — email, SMS, push.
- Design a URL shortener.
- Design a multi-tenant SaaS application.
- The rest of the designs to be able to sketch
- Five sentences that will carry this round
- Design typeahead search for a catalogue of a million products.
- Design live order tracking for a food delivery app.
The frame, every time
- Scope · 5 min. Who uses it, the two or three core flows, read-heavy or write-heavy, roughly how many users. Say what you are excluding.
- Numbers · 3 min. DAU, peak requests per second, data written per day. You are not being tested on arithmetic — on whether you size before you build.
- API · 5 min. The four or five endpoints that matter. Your strength — use it early to set the tone.
- Data model · 10 min. Tables, keys, indexes, and the one access pattern that drives the schema.
- Architecture · 10 min. Client, edge, services, datastores, cache, queue. Draw it.
- Deep dive · 15 min. They pick one piece. Have caching and concurrency ready — those are yours.
- Failure & scale · 7 min. What breaks first at ten times the load, and what you would do.
Design a camp or event booking marketplace.
What they are really testingIf a booking marketplace is on your resume, they will pick this deliberately. Prepare it as your set piece.
Scope it out loud first: browsing and search, viewing availability, booking with payment, and the organiser side for managing listings. Explicitly exclude reviews, messaging and refunds unless they ask.
Core entities:
camps (id, org_id, title, location_id, age_min, age_max, price_cents, status)
slots (id, camp_id, starts_at, ends_at, capacity, remaining, version)
bookings (id, slot_id, user_id, seats, status, idempotency_key, expires_at)
users (id, email, ...)
-- the indexes the access pattern demands
CREATE INDEX ON slots (camp_id, starts_at);
CREATE INDEX ON bookings (user_id, created_at DESC);
CREATE UNIQUE INDEX ON bookings (idempotency_key);
CREATE INDEX ON camps (location_id, price_cents) WHERE status = 'published';Opening with microservices, Kafka and Elasticsearch for a marketplace with a few thousand listings. Every box is a system someone has to run, and the interviewer's next question is "why" — for each one. Start with one database and earn every addition with a number.
“Before I draw anything — who are the users, roughly how many bookings a day, and is this web only or mobile too? … Right. Clients hit an API, the API talks to a database. The entities are users, listings, availability slots, bookings and payments. The main flows are search a listing, view its availability, and create a booking. I would put a cache in front of search because that is the read-heavy part. I am not certain how I would stop two people booking the same slot — I think it needs to be handled in the database rather than in application code.”
Why it passes here: At two to three years they are not grading the design, they are grading decomposition and communication. Naming the entities and the flows is a pass. Saying “I am not certain, and here is the direction I would look” is a pass too — adding a message queue because it sounds senior is not.
“Assume a hundred thousand daily actives and ten thousand bookings a day. That is tiny on writes — a couple per second at peak — but search is read-heavy, call it a hundred to one, so the write path can be a single relational database and the read path needs caching and probably a search index. Postgres for bookings, because the hard requirement is that two people cannot take the same slot, and I want a transaction and a unique constraint on listing and slot rather than an application-level check. The real difficulty is double booking under concurrency: I would take a row-level lock on the slot inside the transaction, and for the checkout window write a short-lived hold row with a TTL so an abandoned cart releases the inventory instead of stranding it.”
Why it lands: Numbers first, then a datastore chosen for the constraint rather than by preference, then the one hard part named and actually solved. That sequence is the round — most candidates draw boxes and add a cache because caches are good, and never say a number.
“The load-bearing requirement here is inventory correctness — the rest is a website. So I would design around that and let everything else follow: a single writer for availability, idempotency keys on the booking endpoint because payment retries are guaranteed rather than possible, and the payment and the booking reconciled through an outbox rather than a distributed transaction, because the failure I actually care about is a customer charged for a booking that does not exist. At ten times this traffic the thing that breaks first is not the database, it is search — that is the piece I would plan to split out, and I would want the seam there from day one. In version one I would deliberately not build dynamic pricing, multi-currency, or recommendations, because each one takes a dependency on availability data and makes the correctness argument harder to hold.”
Why this is the level above: It opens on the risk rather than the boxes, states the failure in the customer’s terms rather than the system’s, says what breaks first at scale, and gives a cut list. Every one of those is a scope decision, and scope decisions are what a staff design round is actually scoring.
The design hangs on three things, and the interviewer will pick one to go deep on:
1 · Search
Faceted filters over location, age band, interest and price. At a thousand listings this is a Postgres query with composite indexes and a GIN index for full-text — and say explicitly that you would not reach for Elasticsearch at that size, because it is a second system to operate and keep in sync for no measurable gain. The crossover is somewhere around a hundred thousand listings, or when you need relevance ranking and typo tolerance rather than filtering. Then the database stays the source of truth and the index is rebuilt from a change stream.
2 · Booking concurrency
The double-booking problem from R5.6: an atomic conditional decrement as the fast path, a unique constraint as the backstop, and an Idempotency-Key so a retried request after a timeout returns the original booking instead of creating a second one.
3 · Payment, which is where most candidates fall down
Never mark a booking confirmed on the client's redirect. The user can close the tab, lose signal, or the redirect can be replayed. The correct flow:
- Create the booking as
pendingand decrementremaining— the seat is held. - Set
expires_ata few minutes out. A background job releases holds that expire, so an abandoned checkout does not permanently consume inventory. - Confirm only on the payment provider's webhook, which must be idempotent because providers retry — verify the signature, then check whether you have already processed that event id.
- Reconcile: a scheduled job that queries the provider for any booking still pending past its window, because webhooks do get lost.
That webhook-and-reconciliation detail is what separates candidates who have shipped commerce from candidates who have read about it.
- What if the payment succeeds but your webhook handler crashes?
- How do you handle a camp in a different timezone?
- Ten times the traffic — what breaks first?
- How would you add "5 people are looking at this slot"?
Design a real-time dashboard for AI agent performance metrics.
What they are really testingIf anything real-time is on your resume, expect this one. Know it cold.
Three parts: ingest, aggregate, deliver.
Ingest. Agents emit events (call started, handed off, resolved, latency, sentiment). Buffer and batch on the client side — one HTTP request per event does not survive volume. Write to an append-only events table or a stream.
Aggregate. This is the key decision: pre-aggregate into time buckets on write rather than computing over raw events on read. Dashboards are read-heavy with a known query pattern, so paying once per event beats paying per viewer. Keep raw events for a short retention window for drill-down, and roll up into minute, hour and day tables beyond that.
events_raw (id, agent_id, type, latency_ms, ts) -- 7 day retention
metrics_minute (agent_id, bucket, calls, handoffs, p95_ms) -- 30 days
metrics_hour (agent_id, bucket, calls, handoffs, p95_ms) -- 1 year
-- the dashboard queries the rollup that matches its window,
-- so "last 24h" reads 24 rows, not 4 million eventsAveraging each agent's p95 to get a team p95. The result is not a percentile of anything, and anyone who works with metrics hears it immediately. Store histograms, or aggregate only counts and sums.
Deliver. Server-sent events, not WebSockets — the flow is one-directional, SSE is plain HTTP, it reconnects automatically, and it passes through proxies that block WebSocket upgrades. Push deltas, not the whole payload. Cache the current bucket in Redis with a short TTL so a hundred open dashboards do not become a hundred identical queries every second.
The detail worth volunteering: percentiles do not average. You cannot compute a p95 across agents by averaging their individual p95s. Either store a histogram per bucket (t-digest or HDR histogram) or accept that you can only aggregate counts and sums. Interviewers who work with metrics notice this immediately.
- How do you handle an agent that goes offline mid-call?
- What if a viewer opens a dashboard for a 90-day window?
- How would you alert on a metric?
Design an e-commerce listing page that survives a traffic spike.
What they are really testingIf commerce work is on your resume, this is that work framed as a scale problem.
CDN in front; static or incrementally regenerated pages for catalogue content; Redis for anything dynamic. The design decision worth articulating is the staleness boundary:
| Data | Acceptable staleness | Therefore |
|---|---|---|
| Product title, images, description | Hours | Static, regenerated on publish |
| Price | Minutes | ISR with a short revalidate, or edge cache |
| Stock count on the listing | Minutes | Cached — an approximate number is fine |
| Stock at checkout | Zero | Authoritative read + reservation, never cached |
| Cart, recommendations | Per user | Client-side after first paint |
Say the sentence: "a design that insists on real-time accuracy everywhere cannot be cached, and therefore cannot handle the spike. The trick is to be precise about which single number has to be exact and when."
"Autoscaling will handle it." New instances take minutes to launch and warm up; a spike arrives in seconds, and the database behind them does not autoscale at all. Caching and a degraded mode survive the first minute. Autoscaling helps with the tenth.
For the spike specifically: pre-warm the cache before a known event, put a queue in front of checkout if the write path is the bottleneck, and have a degraded mode — if the recommendations service is down, render the page without recommendations rather than failing it.
- How do you invalidate a price change across the CDN?
- What is a stampede and how does it show up here?
- How would you handle a flash sale with 10,000 people and 100 units?
Design a notification service — email, SMS, push.
What they are really testingYour queue answer from R5, expanded into a system.
Producers write a notification request; a queue decouples the send from the request path; per-channel workers handle provider specifics; a template service renders content.
The parts that make it a real design rather than three boxes:
- Retries with backoff and a dead letter queue. Permanent failures (invalid address) must not be retried forever; transient ones must be.
- Idempotency. A producer retry must not send twice — dedupe on a notification key.
- User preferences and quiet hours, checked at send time rather than at enqueue time, because preferences may change while a job is queued.
- Per-user rate limiting, so a runaway loop does not send someone four hundred emails. This is the guardrail that has actually saved companies from public embarrassment.
- Provider failover. Two email providers, health-checked, with a circuit breaker.
- Delivery tracking. Provider webhooks for delivered, bounced and complained; a hard bounce should suppress that address permanently.
One retry policy for every failure. A hard bounce retried for hours damages your standing with the email provider; a transient timeout that is never retried loses the message. Classify the failure before you decide what to do with it.
- How do you send 1 million notifications without melting the provider?
- What happens if the template service is down?
- How do you test this without emailing real users?
Design a URL shortener.
What they are really testingThe classic warm-up. It is short, so the differentiator is which detail you volunteer.
Key generation is the interesting part, and there are three answers with real trade-offs:
- Base62 of an auto-increment id. Simple, guaranteed unique, shortest keys — but enumerable, and it leaks how many links you have created.
- Random 7 characters. 62⁷ is about 3.5 trillion, so collisions are rare but must still be handled with a unique constraint and retry.
- A pre-generated key pool. A background job fills a table of unused keys; creation just claims one. No collision check on the hot path, and it is the answer that shows you have thought about write latency.
Reads massively outnumber writes, so this is fundamentally a cache problem: Redis in front, database as source of truth, and the redirect served from the edge.
Hashing the long URL and keeping the first seven characters, with no collision handling. Two different URLs will eventually share a prefix, and the same URL shortened by two users gets one key — which breaks per-user analytics and deletion.
The detail worth volunteering: 301 vs 302. A 301 is cached permanently by browsers, so you get fast redirects and no analytics — the second click never reaches you. A 302 keeps every click observable at the cost of a round trip. If click counting is a product requirement, that decision is forced, and saying so shows you connect technical choices to product ones.
- How would you support custom aliases?
- How do you expire links?
- How would you count clicks without slowing the redirect?
Design a multi-tenant SaaS application.
What they are really testingIncreasingly common, and the answer is a genuine three-way trade-off.
| Model | Isolation | Cost of it |
|---|---|---|
| Shared schema, tenant_id column | Weakest — one missing WHERE tenant_id leaks data across customers | Cheapest to run and migrate. Use row-level security in Postgres so the isolation is enforced by the database, not by remembering. |
| Schema per tenant | Good | Migrations must run N times; connection pooling gets awkward past a few hundred tenants. |
| Database per tenant | Strongest — and the answer for regulated or enterprise customers | Expensive; operationally heavy; per-tenant backup and restore is a feature you now own. |
The pragmatic answer most companies land on, and the one to give: shared schema with row-level security by default, and database-per-tenant as a premium tier for enterprise customers who require it. Then the noisy-neighbour question — per-tenant rate limits and query timeouts, so one customer's report cannot degrade everyone else.
Database per tenant for everyone from day one because it "isolates properly". At a few hundred small tenants that is a few hundred migrations per release and a connection pool per database — operations work nobody budgeted for.
- How do you run a migration across 500 tenant schemas?
- How would you move one tenant to a dedicated database?
- How do you handle a tenant-specific customisation?
The rest of the designs to be able to sketch
What they are really testingBreadth. Any of these can appear; none needs more than a working sketch.
- A rate limiter as a shared service. The R5 answer, plus: where does it live — sidecar, gateway, or a library in each service, and what does the network hop cost you?
- A file upload and processing pipeline. Presigned URL → S3 event → worker → status the client subscribes to. The state machine matters more than the boxes.
- An online learning platform with course search and progress tracking. The interesting part is progress: an append-only event log of "completed lesson X" beats a mutable percentage field, because it survives a course being restructured.
- An authentication service with refresh token rotation. R5.7 as a system.
- A comment thread or activity feed. Cursor pagination, not offset — with new items arriving, offset pagination shows duplicates and skips rows.
- A job scheduler. Cron-like triggers, at-least-once delivery, and the question they always ask: what stops two instances running the same job? (A lock, or a leader.)
- An audit log. Append-only, immutable, with who/what/when/before/after. The follow-up is always retention and how you query it.
Five sentences that will carry this round
What they are really testingNothing — but these are the phrases interviewers write down as evidence of seniority.
- "Let me confirm the scope before I design — are we optimising for reads or writes?"
- "At this scale I would not add that yet. Here is the specific signal that would make me add it."
- "The trade-off I am accepting here is X, and the cost is Y."
- "This data can be stale by a minute; that data cannot be stale at all — and that difference is what drives the whole caching strategy."
- "The first thing that breaks at ten times this load is the database, and here is what I would do about it."
Two habits that matter as much as the content: keep drawing — an interviewer staring at a blank board stops believing you can do this — and ask before you assume. "Is this a global product or single-region?" changes the whole design, and asking it is free.
Design typeahead search for a catalogue of a million products.
What they are really testingWhether you can split a latency budget across the client, the edge and the server, and precompute answers instead of searching on every keystroke.
Scope and numbers first. Suggestions for a prefix, top ten, ranked by popularity, on screen within about 100ms of the keystroke. A million products — but the set of queries worth suggesting is smaller, and more useful to index than raw titles. Every keystroke is a potential request, so suggestion traffic is several times search traffic. That makes this a caching design more than a search design.
The client does half the work. Debounce at around 150ms, cancel the previous request with AbortController so a slow early response cannot overwrite a fast late one, and keep results per prefix in memory so backspacing is free. That alone removes most of the traffic.
Precompute the answers. The top ten for a prefix barely changes minute to minute. So rather than searching at request time, build a map of prefix → top ten offline from query logs and product popularity, and serve it from Redis or straight from the CDN. Long prefixes are rare and can fall through to a real search.
keystroke → debounce 150ms → client prefix cache
→ CDN GET /suggest?q=sho (cached a few minutes)
→ API Redis GET suggest:sho → ["shoes", "shorts", ...]
→ miss search index prefix query → write back to Redis
// offline, every few minutes:
query logs + product popularity → top 10 per prefix → RedisA LIKE '%term%' query against the products table on every keystroke. A plain B-tree index cannot serve it, so it scans, and at typing speed it is the fastest way to take down the database you were meant to be protecting.
The trie question will come. A trie with the top ten stored at each node is the textbook answer and worth sketching — lookup is O(length of the prefix) and the answer is already waiting at the node. Then say what you would actually run: at a million rows Postgres with a text_pattern_ops or trigram index is enough, and a search engine (OpenSearch, Typesense, Meilisearch) earns its place when you need typo tolerance and relevance ranking. Precomputed prefixes in Redis sit in front of either.
Two details worth volunteering: normalise before you key anything — lower case, trimmed, accents stripped — and filter what you suggest. A query log will happily suggest something offensive or a competitor's name, and that is a product incident, not a bug.
- How do you personalise suggestions without losing the cache?
- A product goes viral. How quickly does it appear in suggestions?
- How would you handle typos?
- What does the client show while the network is slow?
Design live order tracking for a food delivery app.
What they are really testingA write-heavy, short-lived data problem. Whether you notice that only the latest position matters, and design for that.
The numbers change the design. Say fifty thousand riders active at peak, each sending a location every four seconds: about twelve thousand writes a second. A customer watching an order reads one rider's position. The data is worthless after a few seconds, and nobody needs every point — only the latest one, plus a thinned trail for disputes.
- Ingest. The rider app sends its position to a stateless ingest service. Batch on the device when the network is poor, and drop stale points on arrival instead of replaying them in order.
- Current position in Redis. One key per rider, overwritten on every update, with a short TTL so a rider who goes offline disappears. A geo set (
GEOADD) lets dispatch ask "which riders are within two kilometres of this restaurant" in one command. - Fan-out to watchers. The customer's app holds a WebSocket or SSE connection for their order. Each update is published to a channel for that order, and the gateway holding the customer's connection forwards it. Most orders have one watcher, so there is no fan-out problem to solve.
- History, thinned. Keep one point every few hundred metres, or every Nth update, in a durable store for disputes and ETA training. Writing all twelve thousand a second into Postgres is the design mistake.
rider app ──every 4s──▶ ingest ──▶ SET rider:{id}:pos EX 30
──▶ GEOADD riders:active lng lat {id}
──▶ PUBLISH order:{orderId}
customer app ◀──SSE / WS── gateway ◀── SUBSCRIBE order:{orderId}
// a sampled trail goes to the durable store, off the hot pathWriting every location update as a row in the main database and having the customer app poll it. Twelve thousand inserts a second of data that is stale in five seconds, plus a read per watcher per poll, all landing on the one system that most needs to stay healthy for orders and payments.
The details that show you have thought about the real device. The phone is unreliable — GPS jumps, tunnels and dead zones — so the server smooths or snaps points to roads before a customer watches a rider teleport across a lake. Battery matters, so the update interval stretches while the rider is stationary. And members of a geo set do not expire on their own: a sweep has to remove riders whose position key has lapsed, or dispatch will assign an order to someone who went home an hour ago.
The ETA is a separate problem — a model over road speeds, restaurant preparation time and the rider's history — and it is fine to say you would scope it out of this hour.
- The rider loses signal for two minutes. What does the customer see?
- How do you find the nearest free rider for a new order?
- How would you keep the trail for a dispute raised six months later?
- What changes on a festival night at five times the load?
Ten questions here; 24 chapters behind them.
- The mental model — how to open, and what the interviewer is scoring
- Capacity estimation — the numbers you are expected to produce without a calculator
- Work two end to end: URL shortener, then feed and chat
- Tradeoff thinking — the difference between boxes and a design
Finished this round?
0 of 27 rounds read in this book