Design a Ticket Booking & Flash-Sale System
like Ticketmaster
Simple to describe, brutal to scale: 2 million people trying to buy the same 50,000 seats in the same ten seconds, and not one of them can be sold twice.
~38 min read · 11 sections · interactive estimator
What the interviewer is testing
"Design Ticketmaster" looks like a booking-system question, and candidates who treat it as one — CRUD an event, CRUD a seat, CRUD an order — miss the point entirely. The hard problem only shows up at one specific moment: the instant a high-demand show goes on sale, when a fixed, tiny inventory (a 50,000-seat arena) is hit by a burst of concurrent demand forty times larger (2,000,000+ people refreshing at once). For 99% of the calendar year this system looks like a boring read-heavy catalog. For the ten minutes that matter, it's a write problem with forty claimants for every seat.
This is a fundamentally different regime from most "design a booking system" interviews. Airbnb-style booking deals with low contention across huge inventory: 150 million distinct listings, and any two guests booking at the same instant are almost certainly booking different properties. The hard problems there are search freshness and geo-partitioning. Ticketmaster is the opposite regime: extreme contention on a tiny, single inventory — every one of those 2 million people is trying to write to the same few thousand rows in the same few seconds. Search and catalog design barely matter here; write arbitration is everything. If you find yourself reaching for Airbnb's playbook (shard by listing, decouple a search index), stop — there is effectively one "listing" and sharding it doesn't dissolve the contention, it just moves it.
| Problem | Why it's hard | What changes at scale |
|---|---|---|
| Write contention on a hot inventory | Thousands of seats, millions of simultaneous claimants; the naive "lock the row, check, write" pattern collapses a relational primary in seconds | The DB can no longer be the point of arbitration; a much higher-throughput layer has to absorb the contention before the DB ever sees it |
| Overselling vs. underselling | Sell a seat twice and you have a legal and PR incident; hold a seat too conservatively and it sits empty while buyers get rejected | The system needs a hold-then-commit model with a precise, auditable definition of the point of no return |
| Fairness under a thundering herd | Every client hits "buy" in the same second; first-come-first-served has to mean something even though network latency and client clock skew make raw arrival order unobservable | Admission has to be paced and ordered deliberately (a queue), not left to whichever request happens to win a race |
Level signal: L4 candidates design a seat table and a "book seat" endpoint but don't notice what happens when 2 million people call it at once. L5 candidates identify the thundering-herd problem and reach for a lock or a queue. L6 candidates recognise that neither locking nor queuing alone is sufficient — they compose an admission gate, a fast contention layer, and a durable commit, and can say which step is reversible. L7/L8 candidates reason about the business tradeoff between strict fairness, seat-map UX ("let me pick a seat" vs. "give me best available"), and system load — and can say what relaxing the zero-oversell requirement would cost the business, and why this one shouldn't.
The rate limiter design post covers the token-bucket and sliding-window mechanics this article's virtual waiting room borrows wholesale for admission pacing (§8) — read that post for the algorithm, this one for how it's repurposed as a fairness queue rather than an abuse shield. And where Airbnb-style booking solves "don't double-book one of 150 million distinct listings," this post solves "don't oversell one of 50,000 seats when 2 million people want them in the same second" — same-shaped booking flow, opposite contention regime, different solution at every layer.
Requirements clarification
Functional requirements
| Capability | In scope | Out of scope (for this interview) |
|---|---|---|
| Event browsing | View an event page, its seat map and pricing tiers | Event search, recommendation and personalised discovery — a standard search index behind CDN-cached event pages, with none of this system's contention |
| Waiting room | On high-demand events, admit buyers into the purchase flow at a controlled rate, in a fair order | Cross-event queue prioritisation, loyalty-tier queue skipping |
| Seat selection & hold | Reserve one or more seats, all-or-nothing, for a short window while the buyer checks out | Group/party seating optimisation ("find 4 seats together") |
| Checkout & payment | Authorize payment, convert a hold into a sold seat, then capture and issue tickets | Full payment gateway internals — see payment processing design for that |
| Cancellation / refund | Cancel an order, release the seat back to inventory | Resale/transfer marketplace |
| Dynamic pricing | Price a section based on live demand signals | The full pricing/ML model — only the interface and consistency implications are in scope |
Non-functional requirements
| NFR | Target | Why this level? |
|---|---|---|
| Overselling | Zero, structurally — not "rare" | A sold-twice seat means turning away a paying customer at the door; the data model must make it impossible, not just unlikely |
| Seat-hold duration | 8 minutes, server-enforced | Long enough to complete checkout including payment entry; short enough that abandoned holds don't starve real demand |
| Waiting-room admission latency | New admission batch every 2–5 s during an on-sale spike | Fast enough that the queue feels alive, slow enough that each batch doesn't itself create a thundering herd against inventory |
| Seat-map render (p99) | < 500 ms after admission | Once admitted, a buyer expects the app to feel responsive; the waiting room already solved the hard latency problem upstream |
| Checkout completion (p99) | < 3 s from "pay" to confirmation | Card authorization dominates this budget; see the payment-processing post for the breakdown |
| Peak scale | 50,000-seat venue; 2,000,000 concurrent buyers at on-sale instant | A representative "superstar tour" on-sale, the scenario that breaks naive designs |
| Durability of a committed sale | RPO = 0: no committed sale lost on database failover | A sale that vanishes in a failover puts its seat back on the market, which is an oversell by another route |
| Availability (on-sale window) | 99.95% during the sale window | An outage during the one ten-minute window that matters is the worst possible failure for this business |
NFR reasoning
Zero structural overselling Drives §5, §6, §7, §10 ›
Unlike an airline overbooking a flight by a known statistical margin, a sold-twice concert seat has no fallback: there's no "next flight" to rebook the loser onto, just a customer standing in a seat someone else is sitting in. The system needs a data model where a second successful write for the same seat cannot happen, however heavy the load.
sold_seats, whose primary key on (event_id, seat_id) rejects any second sale of the same seat (§7), on a database that replicates synchronously so a failover can't lose that row (§10). Everything upstream of that insert — the hold, the queue position — is allowed to be approximate, because it isn't the guarantee.8-minute seat-hold duration Drives §5, §6, §10 ›
Too short and legitimate buyers lose their seat mid-checkout, which under this NFR set means they re-enter contention for a shrinking pool — a bad experience during the exact moment the business most wants a good one. Too long and abandoned holds (someone opens checkout in five tabs, completes one) sit on inventory that a real buyer behind them can't touch.
Waiting-room admission pacing Drives §4, §5b, §9 ›
If all 2,000,000 buyers hit the seat-selection API in the same second, no amount of caching or read replicas saves the write path — the problem isn't serving reads, it's arbitrating writes. Admitting buyers in small batches converts an unbounded burst into a controlled, sized stream that the contention layer in §5 is provisioned for.
Capacity estimation
This is a contention-bound system. The dimensions that matter are how many writers contend for the same tiny set of keys, and for how long; request volume and storage barely register. Two numbers set the shape of everything downstream: the size of the inventory (small — tens of thousands of rows) and the ratio of demand to that inventory (huge — tens of buyers per seat).
Interactive capacity estimator
Key insight: at defaults, the seat-hold layer absorbs roughly 6,000 hold attempts per second (2,000 admitted buyers/sec × 3 attempts each) for the ~12 seconds it takes 25,000 two-seat orders to exhaust 50,000 seats — and none of that burst touches Postgres. The durable store sees only the ~25,000 orders that won, arriving as buyers finish checkout over the following minutes: 2,000 commits/s at the very worst, if every checkout took exactly the same time, and in practice tens to a few hundred per second. Every attempt hits Redis; only winners ever reach Postgres. That split is the architectural justification for §4 and §5: a very fast, non-durable arbitration layer (Redis) in front of a much slower, durable one (Postgres). The estimator counts buyers, not seats, in the contention ratio; measured in seats requested, demand at defaults is 80:1.
Numbers to know
The SLOs in §2 are stated in time, so it's worth being explicit about what each relevant latency number costs, and which decision it settles.
Latency: the numbers that set the budget
| Number | Value | What it settles |
|---|---|---|
| Checkout completion SLO | < 3 s p99 (§2) | The budget every other row below is measured against |
| L2 / Redis hit, same AZ | ~0.1–0.5 ms | A single seat-hold Lua script is a rounding error against the 3 s budget — which is why a buyer can afford a few hold attempts (§5) without threatening the SLO |
| Indexed point read/write, warm DB | 1–5 ms typical, 10 ms+ p99 | Even the durable Postgres commit (§6, §7) is cheap in isolation; the danger at scale is queueing behind lock contention, which §4/§5 keep off Postgres entirely |
| fsync on NVMe with power-loss protection | ~0.1–1 ms | The order-ledger commit's durability cost is negligible against the 3 s budget; almost the entire budget goes to payment authorization (§6) |
| Round trip across AZs, same region | ~0.5–1 ms | What synchronous replication adds to each commit (§4, §10) — cheap against the 3 s budget, which is why RPO = 0 for committed sales costs almost nothing here |
| Cross-region round trip (US↔EU) | ~70–100 ms | A waiting-room admission token has to be verifiable without a cross-region call, or admission alone could eat a few percent of the checkout budget on a single lookup — this is why the token is a signed, self-contained JWT rather than a lookup (§5b) |
Contention: the numbers that shape the design
| Dimension | Estimate | Key insight |
|---|---|---|
| Inventory size | ~50,000 rows | Fits in one Redis node's memory with room to spare — and its ~6,000 hold scripts/s fit one node's single command thread too (§9) |
| Peak contention ratio | ~40:1 | The defining number of this system — this is what makes it a write-arbitration problem, not a scaling problem |
| Seat-hold TTL | 8 min (§2) | Bounds how long an abandoned hold keeps a seat off the market |
| Sell-out window | ~12 s at defaults (2 seats per buyer) | The hold burst is brief; commits trail it for minutes at a far lower rate, so Postgres never sees the spike |
High-level architecture
The architecture separates into four concerns that have almost nothing in common operationally: browsing (cold, read-heavy, ordinary), admission (the thundering-herd gate), seat contention (extreme write throughput, no durability requirement of its own), and commit (low throughput, strict durability requirement). Routing them to separate services lets each be provisioned for what it needs, instead of one service straining to be both very fast and very safe at once.
Component breakdown
API Gateway terminates TLS, authenticates requests, and routes to the right service. It also enforces a first line of pacing — a standard per-IP/per-account rate limit (see rate limiter design) that catches naive scripted traffic before it even reaches the waiting room.
Catalog Service serves event browsing and the seat-map view from read replicas. It's an ordinary read-heavy service, kept separate so it never competes for capacity with the contention path. The seat map's free/held/sold overlay is the one live part; §8 covers how it stays fresh without reading Seat Redis per viewer.
Waiting Room is the admission gate for high-demand events. A buyer without a valid admission token is placed in a queue; the service admits buyers in controlled batches and issues each a short-lived, signed token. It also runs the first bot-defense checks (§10b) before a token is ever issued.
Seat Hold Service is the contention gate. It accepts a seat-hold request only from a caller presenting a valid admission token, and arbitrates concurrent claims using one atomic Redis script that claims all the requested seats or none (§5). It never talks to Postgres: holds, winners' included, live only in Redis until checkout.
Checkout Service owns the correctness-critical commit: it converts a hold into a paid order. It verifies the hold in Seat Redis, authorizes payment with the external gateway, inserts the sale into the Order DB — the commit point — and only then captures the payment (§6). If the insert loses, it voids the authorization and the buyer is never charged.
Admission Redis holds queue position and issued tokens. It's disposable in the sense that losing it degrades admission fairness, not correctness — nothing here is load-bearing for the zero-oversell guarantee.
Seat Redis holds the live state of every seat: free (no key), held (a key with the holder, the quoted price and a TTL), or sold (a key with no TTL). It's the system's highest-throughput store and the only place a hold exists — and, like Admission Redis, an arbitration layer rather than the source of truth. If it's lost, its sold keys are rebuilt from the Order DB (§10).
Order DB (Postgres) is the one component that must not lose a committed write: the durable ledger where the zero-oversell guarantee lives, as a primary key on (event_id, seat_id) in the sold_seats table (§7) that rejects a second sale of any seat. It replicates synchronously so a failover can't drop a sale (§2's RPO = 0). It sees only completed sales; every component in front of it exists to keep it that way.
Kafka carries order events out of the checkout transaction via the outbox pattern (see the equivalent pattern in Airbnb-style booking design): the Checkout Service writes an outbox row in the same transaction as the order commit, and a connector publishes it. Ticket-confirmation notifications and the dynamic-pricing demand signal both consume this stream independently, without adding latency to checkout.
Architectural rationale
Why put Redis in front of Postgres for the hold, instead of locking rows directly? Core tradeoff ›
§3 puts hold attempts at roughly 6,000/sec against a pool of 50,000 keys. A relational database can lock individual rows, but each attempt costs a connection, a transaction and WAL bookkeeping, and thousands per second fighting over a small keyspace exhaust the connection pool and push p99 well past what checkout can tolerate. A Redis instance executing a Lua script has none of that: commands run one at a time on a single thread, so the script completes atomically with no lock manager to contend on. And because a hold lives only in Redis, Postgres never sees a loser or an abandoned hold at all.
sold_seats primary key still rejects a second sale — see §10.Why a separate Waiting Room instead of just rate-limiting the Seat Hold API? Admission design ›
A rate limiter (§1 cross-link) caps throughput but doesn't establish fairness — under a flat rate limit, whichever requests happen to win the race to arrive first get through, and clients that retry more aggressively win more often. The waiting room instead assigns every buyer a queue position on arrival and admits strictly in that order, which is what "first come, first served" requires when arrival order at the network layer is not the same as arrival order at the person's browser.
Why the outbox pattern for order events rather than a direct publish? Decoupling ›
Publishing directly to Kafka inside the checkout request means a slow or unavailable broker blocks a payment confirmation — unacceptable on the highest-stakes request in the system. Writing an outbox row in the same Postgres transaction as the order commit means the event's existence is as durable as the order itself, and a separate connector process publishes it independently, with retry, without the checkout request ever waiting on Kafka.
Real-world comparison
| Decision | This design | Ticketmaster (reported) | Shopify (flash sales) |
|---|---|---|---|
| Admission control | Virtual waiting room, signed queue tokens | "Verified Fan" queue + waiting room product (built on their own queueing layer) | Shopify Queue-it-style waiting rooms for high-demand drops |
| Seat/inventory arbitration | Redis Lua script, all-or-nothing TTL hold | Reported use of in-memory data grids and distributed locks for inventory holds | Redis-backed inventory reservation with short TTL holds |
| Durable commit point | Postgres, primary key on (event, seat) in sold_seats | Relational order/ticketing systems with strict consistency on the seat map | Relational order ledger; oversell tolerated and resolved post-hoc for non-unique SKUs |
| Bot defense | Proof-of-work / CAPTCHA at admission, device + account velocity limits | CAPTCHA, device fingerprinting, purchase-limit enforcement, Verified Fan registration | Bot-detection middleware, checkout velocity limits |
Shopify's flash-sale inventory is usually fungible (any unit of a SKU is interchangeable), so a brief oversell can sometimes be resolved after the fact with a refund and an apology. A concert seat is not fungible — there is exactly one physical seat with that row and number — so this design cannot borrow that tolerance and has to make the zero-oversell guarantee structural rather than probabilistic.
Core algorithm — locking a seat under a thundering herd
Once a buyer is admitted and looking at the seat map, claiming a seat comes down to one question: when several admitted buyers try to hold the same seat within milliseconds of each other, how does exactly one of them win? There are two classic answers — lock first, or check at commit — and this system uses both, at different layers.
sold_seats primary key refuses for any seat already sold.Our choice for this system: both, at different layers. The hold step (§4's Seat Hold Service) takes a pessimistic lock in Redis, because at a 40:1 contention ratio, letting every claimant proceed optimistically means 39 of every 40 buyers pick seats, enter payment details, and only then learn they lost — a worse experience than being told "that seat's gone" in the ~0.3 ms it takes Redis to refuse a SET … NX. The lock is a performance layer, not the guarantee: it lives in Redis, which can fail over and lose recent writes (§10). The guarantee is the commit in §6, an optimistic check in its simplest form — an INSERT into sold_seats, whose primary key on (event_id, seat_id) rejects a second sale of a seat however the two buyers got there. If Redis ever let two buyers hold the same seat, both reach checkout and exactly one insert succeeds; the other buyer's payment authorization is voided, so they're never charged (§6). This is the same "hot-path lock, durable-path constraint" split Airbnb-style booking uses; the difference here is how aggressively the hot path has to turn losers away, because the contention ratio is orders of magnitude higher.
Implementation sketch: all-or-nothing hold + the commit insert ›
-- Step 1: Redis Lua script — all-or-nothing hold (Seat Hold Service)
-- KEYS[1] = "hold:{event_id}:<hold_id>"
-- KEYS[2..n] = "seat:{event_id}:<seat_id>", one per requested seat
-- (the {event_id} hash tag keeps every key in one slot, §9)
-- ARGV[1] = hold_id (derived from the request's idempotency key, so a retry reuses it)
-- ARGV[2] = buyer_id ARGV[3] = price quote, cents ARGV[4] = ttl_ms (480000)
for i = 2, #KEYS do
local v = redis.call('get', KEYS[i])
if v and v ~= ARGV[1] then
return {'TAKEN', KEYS[i]} -- another buyer's hold, or SOLD: claim nothing
end
end
for i = 2, #KEYS do
-- NX: a retry of this same hold claims nothing new and never extends the TTL
redis.call('set', KEYS[i], ARGV[1], 'NX', 'PX', ARGV[4])
end
redis.call('hset', KEYS[1], 'buyer', ARGV[2], 'price', ARGV[3])
redis.call('pexpire', KEYS[1], ARGV[4], 'NX') -- PEXPIRE NX: Redis 7+
return {'HELD'}
-- Step 2: Checkout Service. First: the hold key exists, its buyer matches, every
-- seat key still carries this hold_id, and payment is AUTHORIZED (not captured)
-- for the quoted price. Then one transaction — the commit point:
BEGIN;
INSERT INTO orders (order_id, event_id, buyer_id, total_cents,
payment_auth_id, idempotency_key, status)
VALUES ($order_id, $event_id, $buyer_id, $total, $auth_id, $idem_key, 'pending_capture');
INSERT INTO sold_seats (event_id, seat_id, order_id, price_paid_cents)
VALUES ($event_id, $seat_1, $order_id, $p1),
($event_id, $seat_2, $order_id, $p2); -- one row per held seat
INSERT INTO outbox (aggregate_id, event_type, payload)
VALUES ($order_id, 'order_committed', $payload);
COMMIT;
-- unique_violation on sold_seats_pkey → ROLLBACK, void the authorization, 409.
-- The seat was already sold to someone else; this buyer is never charged.
-- Step 3: capture the authorization → UPDATE orders SET status = 'captured'.
-- Step 4: SET seat:{event_id}:<seat_id> "SOLD:<order_id>" per seat (no TTL),
-- then DEL hold:{event_id}:<hold_id>.
The Lua script is one round trip and runs atomically on Redis's single command thread, so nothing can slip in between "check every seat" and "claim every seat" — the same check-then-act problem the rate limiter's token-bucket script solves for counter increments. Checking all the seats before setting any is what makes a two-seat request all-or-nothing: a buyer never ends up holding one seat of a pair. The Postgres insert is the structural guarantee: whatever Redis believed, the primary key admits one row per seat, and a unique violation tells the Checkout Service unambiguously that it lost.
One open question: what if Redis fails between a hold succeeding and checkout completing? Holds exist nowhere else, so buyers mid-checkout find their hold gone, get a 410, and hold again; the seat keys marked SOLD are reloaded from sold_seats before Seat Redis takes new holds. That's the price of keeping holds out of Postgres: a Redis failure costs some in-flight buyers their holds, and can never cost a double sale. §10 covers this failure mode in full.
API design — the queue token as the interface contract
The admission token shapes this API. It's a capability rather than a lookup: the one endpoint the herd would otherwise flatten, /seats/hold, requires it, and it has to be verifiable without a network call back to the service that issued it (§4's rationale for why).
POST /waiting-room/join
Called when a buyer navigates to a high-demand event that has admission control active. Returns a queue position immediately and a client-side polling interval; no token yet. The queue position is keyed by a deterministic id derived from device and account, not allocated fresh per request, so a retried join after a timeout returns the buyer's existing queue_id instead of allocating a second position — the same double-click problem §10 solves for seat holds, but here it protects queue fairness rather than inventory.
// Request
{ "event_id": "EVT-88213", "captcha_token": "03AGdBq..." }
// Response 200 OK
{
"queue_id": "Q-9f21ab",
"position": 483211,
"estimated_wait_s": 240,
"poll_after_ms": 3000
}
GET /waiting-room/status?queue_id=…
Polled at the client-supplied interval (jittered client-side to avoid every client synchronising into a new thundering herd). Once admitted, this response carries the admission token — a short-lived, signed JWT the client attaches to every subsequent request as a bearer token. Signing means the Seat Hold Service can validate it locally, with no call back to the Waiting Room.
// Response 200 OK — still queued
{ "status": "queued", "position": 12044, "poll_after_ms": 3000 }
// Response 200 OK — admitted
{
"status": "admitted",
"admission_token": "eyJhbGciOi...", // JWT, exp: now + 10 min
"expires_at": "2026-09-17T19:32:00Z"
}
POST /seats/hold
Authorization: Bearer <admission_token> is required and validated by signature + expiry only — no database lookup. The request names specific seats or asks for a quantity of "best available" seats in a section, which the service resolves against Seat Redis (§9). Either way the seats are held all-or-nothing: a two-seat request that finds one seat taken holds neither, so no buyer is left with half a pair. Requests carry an Idempotency-Key, from which the hold_id is derived, so a retried request returns the existing hold rather than competing with it.
// Request
{ "event_id": "EVT-88213", "seat_ids": ["SEC-A-ROW-3-12", "SEC-A-ROW-3-13"] }
// or: { "event_id": "EVT-88213", "best_available": { "quantity": 2, "section": "floor" } }
// Response 201 Created
{
"hold_id": "H-771029",
"seat_ids": ["SEC-A-ROW-3-12", "SEC-A-ROW-3-13"],
"price_quote": 378.00, // stored in the hold record, locked for its duration (§5)
"expires_at": "2026-09-17T19:40:00Z"
}
// Response 409 Conflict — lost the race; nothing was held
{ "error": "seat_unavailable", "message": "One of those seats was just claimed." }
POST /checkout
Authorized by the buyer's ordinary session plus ownership of the hold — not the admission token. Once a buyer holds seats, the hold is the capability, so a token that expires mid-checkout can't strand a buyer still inside their 8-minute hold. Requires an Idempotency-Key header: mobile clients retry on timeouts, and a retried request must never authorize or capture twice. The price charged is the price_quote stored with the hold, not whatever the dynamic-pricing service shows now, so a price change mid-checkout never surprises a buyer.
// Request — Idempotency-Key header required
{ "hold_id": "H-771029", "payment_method_id": "pm_3x91..." }
// Response 201 Created
{
"order_id": "ORD-55123",
"status": "confirmed",
"seat_ids": ["SEC-A-ROW-3-12", "SEC-A-ROW-3-13"],
"total_charged": 378.00
}
// Response 409 Conflict — lost at the commit (§6); authorization voided
{ "error": "seat_sold", "message": "That seat sold to another buyer. Your card was not charged." }
// Response 410 Gone — hold expired before checkout completed
{ "error": "hold_expired", "message": "Your hold on this seat has expired." }
Optional endpoints by level
| Endpoint | Purpose | Level |
|---|---|---|
| GET /events/:id/seatmap | Render the venue seat map with near-live availability (§8) | L4 |
| DELETE /seats/hold/:id | Buyer voluntarily releases a hold early, freeing the seat before TTL expiry | L4 |
| POST /orders/:id/cancel | Cancel a confirmed order: deletes its sold_seats rows, clears the SOLD keys, refunds | L4 |
| GET /waiting-room/status (SSE variant) | Server-push instead of polling, to cut redundant requests at extreme queue sizes | L5 |
| POST /internal/pricing/recompute | Dynamic-pricing service pushes a new price for a section based on the Kafka demand signal | L5/L6 |
| POST /internal/admission/adjust-rate | Ops override to manually widen or narrow the admission rate mid-sale | L7/L8 |
Core flow — from queue to confirmed seat
The dominant path through an on-sale spike, with the three branch points that matter for correctness: losing the hold race, letting a hold expire, and losing at the commit.
Correctness budget: in a consistency-bound flow, the number that matters is which step is the commit point, and what's reversible before it. Steps 1–5 (queueing, admission, viewing, holding) cost nothing to undo: an expired or lost hold returns the seat to the pool with no cleanup. Step 6 authorizes the payment but moves no money, so it's undone by a void the buyer never sees on a statement. Step 7 — one transaction inserting the sold_seats rows, the order and an outbox row — is the commit point, and the sold_seats primary key (§7) is what makes committing a seat twice impossible. Step 8 captures the reserved funds only after the sale is durable, so money is never taken for a seat the buyer doesn't own.
The order — authorize, commit, capture — keeps refunds out of the conflict path. Capturing first would mean every buyer who loses at step 7 has been charged and must be refunded, which takes days to reach their statement. Committing before any payment step would sell seats to cards that then decline. Authorizing first reserves the funds without moving them — a card authorization typically lasts about seven days, far longer than an 8-minute hold — so a lost commit ends in a void and a won commit is guaranteed to be payable. The one remaining gap is a capture that fails after the commit, which is rare because the funds are already reserved; the order waits in pending_capture for a retry sweep (§10).
Data model
Three things are stored durably: what each seat is, which seats are sold, and the orders that bought them. Holds are deliberately absent — they live only in Seat Redis (§5). Access patterns first, since they force the schema below.
| Operation | Frequency | Query shape |
|---|---|---|
| Read seat map for an event | Very high, but served from Redis snapshots (§8), not these tables | All seats for event_id, with live status |
| Hold seats | ~6,000 attempts/s at peak (§3) | Never reaches Postgres — Redis only (§5) |
| Commit a sale | ~25,000 orders per sale at §3's defaults, spread over the checkout window | Multi-row insert keyed on (event_id, seat_id), one transaction with the order |
| Look up a buyer's orders | Low, user-facing "my tickets" page | Point query by buyer_id |
Retry captures stuck in pending_capture | Low, background sweep (§10) | Partial-index scan on status |
| Rebuild Seat Redis's SOLD keys | Rare, recovery only (§10) | Scan sold_seats by event_id |
Two observations force the schema. First, the only hot-path write is the commit: a point insert keyed on (event_id, seat_id), with no range scan or join, which is what makes the primary-key guarantee cheap to enforce. Second, neither the seat-map read nor the hold burst touches Postgres, so this database is provisioned for completed sales rather than for the 6,000-attempts-a-second burst in front of them.
Sale state needs a companion table for the seat's identity — which section, row and seat number a given seat_id refers to, and its base price tier — versus its fast-moving state. The catalog is reference data, written once when an event is set up and effectively read-only afterward; it's what §4's Catalog Service loads to seed Seat Redis and render the seat map, and what a hold request's seat_id is validated against before it's even worth attempting a lock.
-- Reference data: what a seat IS, seeded once when the event is set up
CREATE TABLE event_seats (
event_id BIGINT NOT NULL,
seat_id TEXT NOT NULL, -- e.g. 'SEC-A-ROW-3-12'
section TEXT NOT NULL,
row_label TEXT NOT NULL,
seat_number INT NOT NULL,
price_tier TEXT NOT NULL,
map_x NUMERIC, -- seat-map render coordinates
map_y NUMERIC,
PRIMARY KEY (event_id, seat_id)
);
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
buyer_id BIGINT NOT NULL,
event_id BIGINT NOT NULL,
total_cents INT NOT NULL,
payment_auth_id TEXT NOT NULL, -- gateway authorization, captured after commit
idempotency_key TEXT NOT NULL UNIQUE,
status TEXT NOT NULL, -- 'pending_capture' | 'captured' | 'cancelled'
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_orders_buyer ON orders (buyer_id, created_at DESC);
CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'pending_capture';
-- Source of truth for the zero-oversell guarantee: one row per sold seat
CREATE TABLE sold_seats (
event_id BIGINT NOT NULL,
seat_id TEXT NOT NULL,
order_id BIGINT NOT NULL REFERENCES orders,
price_paid_cents INT NOT NULL, -- the hold's quote, not today's price
sold_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (event_id, seat_id), -- the structural guarantee
FOREIGN KEY (event_id, seat_id) REFERENCES event_seats
);
CREATE TABLE outbox (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
aggregate_id BIGINT NOT NULL, -- order_id
event_type TEXT NOT NULL, -- 'order_committed', 'order_cancelled'
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Why is the primary key (event_id, seat_id), not a synthetic id? ›
The overselling guarantee has to be enforced by the database itself, not by application logic that could have a bug. Making (event_id, seat_id) the primary key of sold_seats means a second INSERT for the same seat is rejected at the constraint level — no code path, however buggy, can record two sales of one seat. Rows are only ever inserted, and deleted on cancellation, so there's no status column for a stale update to flip: "a second insert is rejected" is the whole guarantee.
Why aren't holds stored in Postgres too? ›
Writing each winning hold to Postgres would make it a second copy of state Redis already arbitrates. Converting a hold into a sale would then be an UPDATE whose correctness depends on a status guard, and every abandoned hold would leave a row for a sweep to clean up. Keeping holds only in Redis means Postgres sees one insert per sold seat, and the constraint check is a plain insert. The cost is that a Redis failure loses in-flight holds (§10): those buyers hold again, which is an inconvenience rather than an oversell.
Why record the price paid on each sold seat? ›
Dynamic pricing (§4's pricing consumer) can change a section's price between the moment a seat is held and the moment checkout completes. The hold record in Redis carries the quote from the instant it's acquired (§5), and the commit copies it into price_paid_cents, so the buyer is charged what they saw and a partial cancellation knows what each seat cost — the same pattern as the price_quote_id field in Airbnb-style booking's booking request.
Write-path optimisation — pacing admission and absorbing the spike
The catalog is an ordinary cold read path, and the one hot read — the seat map — is covered at the end of this section. The main optimisation target is the write side: turning an unbounded, synchronised burst of demand into a stream every downstream component is provisioned for.
Admission pacing is a token bucket, reused
The Waiting Room's admission rate (§3, §5b) is implemented with the token-bucket mechanism from rate limiter design, keyed per event instead of per user: tokens refill at the configured admission rate, and each refill tick admits the next batch of queued buyers in strict position order. The difference from a standard rate limiter is intent — there, the bucket protects the system from abuse; here, it protects the system from its own legitimate traffic, and a full bucket means "let more people in," not "reject this request."
Why admission comes in batches
A buyer only learns they've been admitted when their client next polls, every ~3 s (§5b), so admitting continuously gains nothing over admitting in batches at that cadence. Batching every 2–5 s (§2's admission-latency NFR sets the window) turns admission into one write per batch rather than one per buyer: advance the event's admitted-up-to position by ~6,000 (2,000/s × 3 s), and each status poll compares the buyer's queue position against that single counter before issuing a token.
Admission rate: fixed from §3, with a manual override
The admission rate is set before the sale from §3's arithmetic — the rate Seat Redis and checkout are provisioned for — and operators can change it mid-sale through §5b's override endpoint while watching Seat Redis p99 and checkout error rates. At this article's numbers one Seat Redis primary runs at a small fraction of its capacity (§9), so a fixed rate with a human override is enough.
When the override becomes an automatic control loop L7 aside ›
If the margin were thin — many on-sales sharing capacity, or a hold path much more expensive than one Lua script — the override becomes a loop: the Seat Hold Service reports Redis p99 and checkout's commit latency, and the Waiting Room narrows admission when either crosses a threshold and widens it as they recover. The loop needs hysteresis and a floor on the rate, or it oscillates and can starve the queue entirely. That operating risk is why it isn't the default.
Connection pooling for the commit path
Postgres sees only commits — ~25,000 orders per sale at §3's defaults, spread over the minutes buyers take to check out. A transaction-mode connection pooler (PgBouncer) keeps the backend connection count bounded however many Checkout Service instances are running, so a cluster of simultaneous checkouts queues briefly in the pooler instead of exhausting Postgres's connection limit before its throughput.
Keeping the seat map fresh without reading Seat Redis per viewer
Every admitted buyer loads the seat map, and it goes stale the moment seats start going. Reading 50,000 keys per page view would put that load on the store that's busy arbitrating holds. Instead, a snapshotter reads each section's state from a Seat Redis replica once a second into a bitmap — one bit per seat, available or not, about 6 KB for the whole venue — served from a CDN-cacheable endpoint, with per-second deltas pushed to open pages over server-sent events. A buyer can still click a seat that went a second ago; the hold returns 409 in under a millisecond and the map repaints. The map is advisory, and the hold is the only check that counts. Clients retrying after a 409 add random jitter, so losers from the same batch don't re-collide in lockstep.
Why not add Postgres read replicas, the usual first move for a database under load? Because the load here is a modest stream of commit inserts, not reads, and the one hot read — the seat map — comes from Redis snapshots. A synchronous standby already exists for durability (§2's RPO = 0); more replicas would add replication work to the primary during the sale and take no load off it.
Deep-dive scalability — one Seat Redis per on-sale, and the waiting room
Everything contended in this system fits on one Redis primary. At §3's defaults that's 50,000 seat keys and ~6,000 hold scripts a second, each a handful of commands; one primary sustains on the order of 100,000 simple commands a second on a single core, so the hold path uses a fraction of it with room for several times §3's attempt rate. The design runs one Seat Redis primary per on-sale, plus a replica for failover, and keys every seat as seat:{event_id}:<seat_id>. The {event_id} hash tag puts all of an event's keys in one Redis Cluster slot, which is what lets a multi-seat hold script touch several seats atomically — Redis rejects a script whose keys span slots.
"Best available" runs inside that same primary as one script. Most flash-sale buyers ask for "2 tickets, best available" rather than picking a seat, and the obvious implementation — rank the seats in the service, then try to hold the top pair — sends every buyer at the same few premium seats, where all but one lose and retry. Instead, each section's free seats sit in a sorted set scored by seat quality, and the hold script pops the best N and claims them in the same atomic step, so two buyers asking at the same instant get different seats rather than colliding. A seat whose hold expires goes back into its set, driven by a second sorted set ordered by hold expiry.
The component that needs real scaling work is the waiting room, because it sees all 2,000,000 buyers rather than the admitted few thousand per second. Polling every ~3 s, the queue generates on the order of 670,000 status checks a second — by far the largest raw request volume in the system — while the seat store handles about a hundredth of that. Geo-distribution is deliberately avoided for the hot path: one event is one small, contended keyspace, and spreading it across regions would add consensus latency to every hold.
Big on-sales also overlap — a stadium tour and an arena show can open the same Friday morning. Each on-sale gets its own admission bucket and its own Seat Redis primary, provisioned ahead of time (on-sales are scheduled weeks out, so this is planning rather than autoscaling). The point is isolation: one sale's backlogged waiting room or busy Seat Redis must not slow down a second sale.
If one event outgrew one Redis primary: sharding seats and the hot-shard problem L6+ aside ›
Nothing at this article's numbers needs this, but an interviewer may push on it: what if one event needed ~100,000 hold scripts a second or more — a far bigger inventory, or a much higher admission rate? Then seats shard across several Redis primaries, and section is the natural split, because a multi-seat hold stays within one section. The hash tag becomes {event_id:section}, so each section is its own slot.
Sharding exposes a problem exact-seat picks don't have: "best available" concentrates on whichever shard owns the top-ranked sections, right up until their inventory runs out. Two mitigations apply. Sub-partition a hot section (front-left, front-centre, front-right as separate slots) so "best available in the front section" isn't one physical shard. Serve ranking reads from replicas so they don't compete with hold scripts on the primary.
Scaling the waiting room itself ›
At 2,000,000 concurrent buyers, even a lightweight polling endpoint (§5b) receives on the order of 2 million status checks within a single 3-second polling interval — roughly 670,000 a second, effectively the full waiting-room population, since only ~25,000 of 2,000,000 buyers are admitted before the sale sells out (§3). Admission Redis is sharded by queue_id hash, which is safe in a way seat sharding isn't: a buyer's queue position has no relationship to any other buyer's, so nothing concentrates. The admitted-up-to counter (§8) is one key per event, read on every poll, so the Waiting Room caches it in-process for a fraction of a second rather than reading Redis 670,000 times a second. The larger cost is connection fan-out at the API Gateway layer; a server-sent-events variant (§5b's optional endpoint) trades a slightly more complex client for far fewer redundant polling round trips at this scale.
Geo-distribution ›
Unlike a globally distributed system, one event has one venue and one on-sale instant — there's no natural geographic partition of the workload the way there is for, say, a booking system with listings worldwide. The pragmatic approach is to run the hot path (Waiting Room, Seat Hold, Checkout) for a given on-sale event in a single region close to the majority of expected demand, accepting higher latency for a minority of buyers, rather than attempting cross-region coordination on a keyspace this small and this contended — cross-region consensus on a single 50,000-row hot table would be strictly worse than the latency cost it's trying to avoid.
Failure modes and edge cases
One principle runs through every row: Redis can be wrong, Postgres can't. A failure in front of the commit may cost a buyer a hold or a retry; none may produce two sales of one seat, or a charge without a seat.
| Scenario | Problem | Solution | Level |
|---|---|---|---|
| Buyer double-clicks "hold" | Two identical hold requests race each other | Both carry one Idempotency-Key, hence one hold_id; the Lua script (§5) treats that hold's seats as already its own |
L4 |
| Seat Redis primary fails mid-sale | Holds live only here; the newest SOLD keys may be lost too | Buyers hold again. Reload SOLD keys from sold_seats before taking holds; the primary key catches anything missed |
L5 |
| Two buyers reach checkout holding the same seat | A Redis failover, or a hold that expired mid-checkout and was re-held | The second sold_seats insert fails the primary key: roll back, void that authorization, return 409. Nobody is charged |
L5/L6 |
| Payment gateway times out on authorize or capture | The outcome is unknown; a blind retry could charge twice | Retry with the same gateway idempotency key, which returns the original result rather than acting twice | L5/L6 |
| Capture fails after the commit | The seat is sold but no money has moved | The order stays pending_capture; a sweep retries within the ~7-day authorization, and cancels the order if declined |
L6 |
| Order DB primary fails | Asynchronous replication could drop sales committed just before failover | Synchronous replication: a commit returns only once the standby has it, so failover loses nothing (RPO = 0) | L6 |
| Clock skew between token issuer and verifier | Valid admission tokens rejected, or expired ones accepted | NTP-synced hosts plus ~5 s leeway on JWT expiry; server-relative TTLs everywhere else | L6 |
| The on-sale instant is a scheduled surge | Autoscaling reacts in minutes; this goes from zero to peak in under a second | Pre-warm Seat Redis, Checkout replicas and the Postgres pool before the scheduled time | L7/L8 |
Why reload SOLD keys if the primary key already prevents oversell? ›
Correctness doesn't need the reload; the buyer experience does. A replica promoted after a failover may be missing SOLD keys written in the last moments before it (Redis replicates asynchronously). Without the reload, those sold seats look free: buyers hold them, fill in payment details, and only learn at checkout that the seat was gone — a 409 where §5's design promises a sub-millisecond one at hold time. Loading sold_seats for the event before the new primary accepts holds takes a single scan of at most 50,000 rows. In-flight holds can't be recovered the same way, because they were never written anywhere else; that's the cost §7 accepted for keeping holds out of Postgres.
Abuse, trust and compliance
The threats here are dominated by two things: this is a money-movement system (fraud, chargebacks), and it's also a scarce-resource allocation system that automated buyers have a strong financial incentive to game (scalping). Five threats, each with a headline defense: queue-stuffing bots → CAPTCHA/proof-of-work plus per-account velocity limits at admission; hold hoarding → a per-account hold cap enforced inside the hold script; admission-token theft or forgery → signed, event-scoped, short-lived tokens; payment fraud and chargebacks → idempotency keys and velocity checks before authorization; price scraping → a rate-limited price endpoint that never exposes demand signals. The reasoning and interview probes for each are in the accordions. Entry validation at the venue — rotating barcodes, scanner redemption — is a separate system with its own store, out of scope here.
Bot and scalper defense at the admission gate ›
The cheapest place to stop a scalping bot is before it ever consumes a queue position. POST /waiting-room/join (§5b) requires a passed CAPTCHA or proof-of-work challenge before issuing a queue position. Per-account and per-device velocity limits (INCR joins:{account_id}:{event_id} with a TTL matching the sale window, rejecting past a small cap like 4 joins) stop one identity from occupying dozens of queue slots. Purchase-quantity limits are enforced again at checkout (max N tickets per account per event), independent of admission, so a bot that gets past the gate is still capped downstream.
Interview probe (L5): "How do you stop a script from generating 10,000 queue positions?" — the answer has to name a specific identity-binding mechanism (device fingerprint, verified account, CAPTCHA), not just "rate limit it," since a naive IP-based rate limit is trivially defeated by rotating IPs.
Hold hoarding ›
A hold costs nothing, so a bot with admitted accounts can hold seats it never means to buy, keeping them off the market 8 minutes at a time and re-holding as they expire — a way to push buyers toward resale listings. Three cheap defenses. A per-account cap on seats held at once, checked inside the hold script itself: one more key, holds:{event_id}:<account_id>, compared and incremented in the same atomic step as the claim, so parallel requests can't race past it. A short re-hold cooldown on a seat an account just let expire. And the hold-to-purchase ratio per account, fed to scalper detection, since real buyers convert most holds and hoarders convert almost none.
Interview probe (L6): "What stops one person holding the whole floor without paying?" — the expected answer puts the cap inside the atomic hold rather than in a separate check that parallel requests can race, and notes that shortening the TTL alone hurts real buyers more than bots.
Admission-token forgery and replay ›
The admission token (§5b) is a bearer credential that grants access to the hot path — if it leaked or could be forged, it would defeat the entire admission gate. It's signed (so it can't be forged without the signing key), scoped to one event_id and one queue_id (so it can't be replayed against a different sale), and short-lived (so a leaked token has a small blast-radius window). A stolen token is a real risk (screen-shared or sold on secondary markets) but is bounded: it grants queue-skip access to one sale for a few minutes, not standing access to the system.
Interview probe (L6): "Who is trusted with a valid admission token, and what happens when that trust is abused?" — the expected answer distinguishes "the token proves you waited your turn" from "the token proves who you are," and recognises the system doesn't need the second guarantee until checkout, where the buyer's session and payment identity authenticate them.
Payment fraud and chargebacks ›
This is the same problem the payment processing post covers in depth — idempotency keys prevent double-charges, and card-testing/velocity fraud checks run before authorization. The ticketing-specific addition is that a chargeback also has to delete the sale's sold_seats rows, releasing the seat back into inventory (or, if the event already happened, it doesn't — that's a business rule, not a systems one) and is flagged for the scalper-detection pipeline, since chargeback rate per account is a strong bot signal.
Dynamic-pricing scraping and manipulation ›
A price-lookup endpoint that's cheap to call and reveals the live pricing algorithm's internals invites two abuses: scraping the full pricing surface to arbitrage resale pricing, and hammering the endpoint hard enough to affect the demand signal the algorithm itself consumes (§4's Kafka-driven pricing consumer). Price checks are rate-limited per identity like any other endpoint, and the response only ever returns a current price, never the demand signal, trend data, or ranking internals that would let a scraper reverse-engineer the model.
Interview probe (L7): "How would you detect systemic price-scraping across the whole population, not just one abusive account?" — the expected answer reaches for aggregate anomaly detection on the pricing-endpoint access pattern (e.g. a stream job over the Kafka access log flagging accounts or IP ranges with abnormal call-to-purchase ratios), not a per-request check.
Compliance and retention ›
Routing card data through the external payment gateway (§4) rather than handling it directly keeps this system out of full-scope PCI-DSS — it qualifies for the lightest self-assessment tier (SAQ-A) because cardholder data never touches its own stores. Order and buyer-PII records are retained for a fixed window (years, for tax and chargeback-dispute purposes); a deletion request is handled at the identity-service layer, out of scope for this article but named here so it's clear it wasn't overlooked.
How to answer by level
The mechanisms don't change much across levels — what changes is whether they're composed into one coherent contention story or reached for independently.
L4 ›
- A schema for events, seats and orders, and a working hold → checkout flow with an explicit TTL on the hold
- Recognises that "book a seat" needs a lock or constraint at all — doesn't just write and hope
- Names no specific mechanism that makes a double sale impossible
- Doesn't yet reason about what happens when 2 million people call the same endpoint at once
L5 ›
- Identifies the thundering-herd problem unprompted and proposes a queue or rate limit for admission
- Proposes an atomic lock (Redis
SET NX PXor equivalent) for the seat-hold step, with a TTL - Backs the lock with a unique key on
(event_id, seat_id)in the database - Knows a checkout endpoint needs an idempotency key
- Has both mechanisms but can't yet say which one is load-bearing, or what happens if the lock's store fails
L6 ›
- Separates the fast, non-durable contention gate (Redis) from the durable commit point (Postgres), and can say which failures each layer tolerates
- Orders payment around the commit — authorize, commit, capture — and says what a buyer who loses at the commit experiences
- Holds multi-seat requests all-or-nothing, and resolves "best available" without every buyer colliding on the same seats
- Solves each failure mode in isolation rather than as instances of one underlying design principle
L7/L8 ›
- Frames the whole system around one principle — keep contested work off the durable store — and derives every other component from it
- Treats the on-sale instant as a planned capacity event requiring pre-warming, not something reactive autoscaling can handle
- Treats the admission rate as an operational dial the team owns across on-sales: set from the previous sale's measured Redis p99 and commit throughput, rehearsed with a load test before a major on-sale, with a named owner allowed to change it live
- Reasons about the business tradeoff between strict fairness, seat-picking UX, and system load as a genuine three-way tension
- Prices each guarantee: synchronous replication adds a cross-AZ round trip (~0.5–1 ms) to every commit, and the answer explains why that's cheap against a lost sale — and what the business would accept by relaxing it
Classic probes, level-differentiated answers
| Question | L4 | L5 | L6 | L7/L8 |
|---|---|---|---|---|
| "Two buyers click 'buy' on the same seat at the same millisecond. Walk me through exactly what happens." | The database should reject the second write — doesn't yet name a specific mechanism | Redis SET NX rejects the loser in under a millisecond; a unique key on (event_id, seat_id) backs it up |
Says which is load-bearing: the lock is a performance layer, the sold_seats insert is the guarantee, and a loser at commit gets a voided authorization, not a refund |
Ties the split to §3's numbers — ~6,000 hold attempts a second never reach Postgres, which sees ~25,000 inserts per sale — and argues what a Postgres-only design would cost at 40:1 contention |
| "How would you change this design for a system that could tolerate a small oversell, like a flight?" | Keeps the same lock; unsure what would change if oversell were acceptable | Knows airlines overbook on purpose, and that the per-seat key would become a count checked against a limit | Recognises that relaxing zero-oversell (§2) would let §5's pessimistic lock go in favour of pure optimistic checking, trading a rejected-at-commit UX for a simpler hot path | Quantifies it: the oversell rate at which dropping Redis outweighs a worse rejected-at-commit UX, tied to the business's risk tolerance rather than a fixed threshold |
| "The Waiting Room's Redis cluster goes down entirely during a sale. What happens?" | Says the sale breaks — doesn't separate what stops from what keeps working | New admissions stop; buyers already admitted can keep holding and checking out | Separates correctness from availability: Seat Redis and Postgres don't depend on Admission Redis, so holds and checkouts in flight are unaffected, and admission resumes on recovery | Also reasons about recovery: resuming admission without re-admitting buyers who hold a valid token, and whether failover changes fairness for buyers mid-queue |
| "How do you decide the admission rate number itself?" | Picks a number without deriving it from anything in the system | Derives it from §3's capacity numbers as a fixed calculation | Derives it from what Seat Redis and checkout are provisioned for, and names the signals — Redis p99, checkout errors — that would justify overriding it mid-sale | Treats it as an operational dial: set from the last sale's measurements, rehearsed under load, with a named owner — and knows when an automatic loop would earn its hysteresis (§8) |
sold_seats insert, placed between payment authorization and capture (§6)→a primary key on (event_id, seat_id) is the guarantee, whatever Redis believed (§7)sold_seats after a Redis failure (§10)- Rate Limiter System Design, atomic Redis operations, distributed race conditions, and multi-tier quota enforcement
- URL Shortener System Design, hash encoding tradeoffs, database sharding strategies, and viral key mitigation
- Web Crawler System Design, Bloom filter deduplication, politeness throttling, and distributed frontier design
- Twitter/X Feed System Design, fan-out write amplification, hybrid push/pull strategy, and celebrity threshold design
- Notification Service System Design, multi-channel delivery, idempotency keys, and priority queues at scale
- Search Autocomplete System Design, Trie data structures, prefix caching, and read-heavy scale strategies
- Key-Value Store System Design — quorum consensus, LSM trees and SSTables, and anti-entropy repair
- Distributed Cache System Design — consistent hashing, eviction and TTL policy, hot keys, and cache-aside vs write-through
- Chat System (WhatsApp) System Design, WebSocket management, transient vs persistent storage, and read receipts
- Video Streaming (YouTube) System Design, ABR streaming, CDN distribution, and metadata management
- Distributed Message Queue System Design, Kafka partition tuning, exactly-once delivery, and geo-replication
- File Storage (Dropbox / Google Drive) System Design, chunking, delta sync, conflict resolution, and global deduplication
- Ride-Sharing System Design (Uber / Lyft) — geohashing, WebSocket-driven location tracking, and ETA prediction
- Payment Processing System Design — idempotency keys, exactly-once semantics, and append-only ledger models
- Top-K Leaderboard System Design — Redis sorted sets, approximate counting, and stream aggregation
- Airbnb Booking & Reservation System — inventory locks, double-booking prevention, and async elasticsearch sync
- Photo-Sharing Feed System Design — image pipelines, CDN delivery, and social graph scaling
- Proximity Search System Design (Yelp / Google Places) — geohash indexing, quadtree partitioning, and Bayesian review ranking
- Online Judge System Design — secure sandboxing, execution queues, and worker scaling
- Collaborative Document Editing (Google Docs) System Design — operational transformation vs CRDTs, ownership leases, and offline merge
- Google Maps System Design — road-graph routing, contraction hierarchies vs customisable preprocessing, vector tiles, and live-traffic ETAs
- Object Storage (Amazon S3) System Design — erasure coding across zones, durability math, and a range-partitioned metadata index
- Social Post Search System Design — per-viewer visibility filtering, over-fetch math, and fan-out tail latency