Design Ticketmaster
Design Ticketmaster (event ticketing under a flash crowd)
The whole system is easy right up until it isn't. Selling movie tickets on a Tuesday afternoon is a CRUD app: read seats, pick one, write a row, done. Then a Taylor Swift stadium tour goes on sale and 50,000 people click "buy" for the same 10,000 seats in the same three seconds — and the naive design, the one that reads the seat, sees "available," and writes "booked," sells the same seat to five different people. Two of them show up to the stadium with a valid barcode for seat 114-C. That is the interview: not "can you model a ticket," but "can you make one seat go to exactly one buyer when tens of thousands of buyers race for it at once." This lab derives the design that survives the flash crowd, and quantifies exactly how badly the obvious approach fails before we fix it.
1. The Trap — check-then-book quietly sells the same seat five times
Here's the code everyone writes first. It looks obviously correct:
// NAIVE booking — check, then book
seat = db.query("SELECT status FROM seats WHERE seat_id = ?", seatId)
if (seat.status == "AVAILABLE") { // (A) read
db.exec("UPDATE seats SET status='BOOKED', owner=? WHERE seat_id=?", user, seatId) // (B) write
return "you got it!"
}
return "sold out"
The bug is the gap between (A) and (B). Between reading "available" and writing "booked," nothing stops another request from reading the same "available." Two buyers both pass the if, both run the UPDATE, both get "you got it!" — the second write simply overwrites the first owner. One seat, two barcodes.
Let's quantify it, because "it's a race, it could happen" is not an interview answer — a number is. Take one hot on-sale: N = 50,000 buyers converge on S = 10,000 seats, and the booking attempts land over a burst window of T = 2 seconds. The read-to-write gap (A→B: network hop + app logic + the UPDATE round-trip) is a modest δ = 10 ms.
Assume buyers spread evenly over seats (the OPTIMISTIC case — reality is far worse,
everyone rushes the front rows):
attempts per seat r = N / S = 50,000 / 10,000 = 5
pairs of attempts/seat C(5,2) = 10
P(two attempts collide) ≈ 2δ/T = 2(0.01)/2 = 0.01 (both land within 10 ms of each other)
double-booked seats ≈ pairs × P × S
= 10 × 0.01 × 10,000
≈ 1,000 seats sold to two people
~1,000 of the 10,000 seats — 10% of inventory — double-sold in the first two seconds, and that's the best case where buyers are spread perfectly evenly. Everyone actually fights over the front-section "best available" seats, so a single hot seat can take 50+ simultaneous attempts and be sold a dozen times over. "The window is only 10 milliseconds, it'll basically never happen" is the exact intuition that fails: a tiny per-attempt window, multiplied by tens of thousands of attempts hammering the same rows, is a mathematical certainty of mass double-booking. This is not a rare edge case. It is what your naive design does on its most important day.
2. Scope it like a senior (ask before you draw)
Don't reach for the whiteboard. Each of these answers changes the design materially:
- Assigned seats or general admission? Reserved-seating (row/seat 114-C) means per-seat state and per-seat contention. General admission is a pure inventory counter (N seats left, decrement) — a much easier problem. The hard, interesting version is assigned seats; assume that.
- Is there a hold/reservation step, and what's the timeout? Can a buyer lock seats for a few minutes while they enter payment, or is it one-shot atomic purchase? A hold makes the UX humane but introduces a timed state (and an expiry race). Assume a hold with a 5-minute TTL.
- Is payment in the critical path of holding a seat? If you must charge the card before the seat is yours, the "lock" is held for the entire slow payment flow (seconds). If payment happens after a fast hold, the lock is held for milliseconds. This decides your contention budget — pin it down.
- Do we need a virtual waiting room for flash sales? Is the expectation that everyone gets a fair shot in FCFS order, or is "sold out, try again" acceptable? A waiting room is a load-shedding + fairness mechanism, and it's the difference between a controlled drain and a stampede.
- Scale: peak concurrent buyers on a hot event? Steady-state tickets/month? Reads (browsing seat maps) vs writes (bookings) ratio? Assume 10M tickets sold/month, ~3B seat-map page views/month (a ~300:1 read:write ratio), and a hot on-sale of ~50K concurrent buyers against a single event.
Locked assumptions for this lab: assigned seats; a 5-minute hold before payment; payment happens after the hold (hold is cheap, held for ms not seconds); a virtual waiting room for flash on-sales; 10M tickets/month steady, 50K concurrent buyers peak on one hot event; strong consistency on the seat (never double-book) is non-negotiable, availability of the browse path can degrade gracefully.
3. Reason to the design (derive the seat-hold state machine, don't recite it)
Step 1 — a seat is a tiny state machine, not a boolean. "Available / booked" is too coarse: it has no room for "someone is mid-purchase." Model three states:
AVAILABLE —holdSeat→ HELD(owner, expires_at = now+5min) —confirmBooking→ BOOKED
↑______________________________________|
TTL expiry OR explicit release
HELD is the insight: it's a temporary, owned, self-expiring reservation that gives a human time to pay without letting them squat a seat forever. BOOKED is terminal. The expiry edge (HELD→AVAILABLE after 5 min) is what recycles seats abandoned in checkout.
Step 2 — the crux: making the AVAILABLE→HELD transition atomic. Everything in this problem reduces to one question: how do two concurrent holdSeat calls on the same seat get serialized so exactly one wins? Three real mechanisms, in order of how I'd reach for them:
- (a) Conditional / compare-and-set UPDATE (optimistic). Collapse the read and the write into a single atomic statement:
There is no gap for a second buyer to slip into: the database takes a row write-lock, re-evaluatesUPDATE seats SET status='HELD', held_by=?, hold_expires_at = now()+interval '5 min' WHERE seat_id = ? AND status='AVAILABLE'; -- the WHERE re-checks under the row's write lock -- then: if affected_rows == 1 → you got the hold; if 0 → someone else didstatus='AVAILABLE'at write time, and only one transaction's predicate can be true. The loser seesaffected_rows = 0and is told "sold." One round trip, no explicit lock object. This is the cleanest atomic hold for a single seat row. - (b) Pessimistic
SELECT ... FOR UPDATE. Take an explicit row lock, check status, update, commit — the lock serializes concurrent transactions on that row. Functionally equivalent guarantee to (a); you reach for it when the hold decision spans multiple statements/rows in one transaction (e.g. "hold these 4 adjacent seats or none"). - (c) Distributed lock (Redis
SETNX/Redlock per seat). Grablock:seat:Xwith a TTL before touching the DB. Fast and offloads the database, but Redis is now a second source of truth and its failover semantics are fuzzy (a lock can be lost on failover and two holders emerge). Fine for the ephemeral hold if you still commit the actual sale transactionally in the DB — never as the sole guardian of the money.
Step 3 — a hold must expire, and expiry is a background sweep, not magic. A HELD seat carries hold_expires_at. A worker (or a lazy check on the next read) flips HELD→AVAILABLE once hold_expires_at ≤ now. Because every hold uses the same fixed 5-minute TTL, insertion order equals expiry order — a single ordered structure (or an indexed scan on hold_expires_at) makes the sweep cheap.
Step 4 — shed the stampede with a virtual waiting room. Even with a perfectly atomic hold, 50K buyers slamming one event's seat rows will melt the database on lock contention and connection-pool exhaustion (traced in Break It #3). The fix is upstream: a virtual waiting room queues arrivals and admits them to the booking service at a metered rate (say, a few thousand/second) in FCFS order. It converts a stampede into a controlled drain and gives you fairness for free. The atomic hold guarantees correctness; the waiting room protects availability. You need both.
4. Build it — the design worksheet
This is the artifact you'd produce on the whiteboard in a 45-minute round. Numbers are traced, not asserted — recompute them before an interview.
Functional requirements
- Browse events; view a live seat map for an event (which seats are available/held/booked).
holdSeat: atomically reserve one or more available seats for a buyer for 5 minutes.confirmBooking: convert a valid, unexpired hold into a permanent booking after payment succeeds.- Automatically release expired holds back to inventory.
- A virtual waiting room that admits buyers to a hot on-sale fairly (FCFS) and at a safe rate.
Non-functional requirements
- Correctness (hard): a seat is BOOKED by at most one buyer, ever. No double-booking under any concurrency.
- Availability: browsing/seat-map can be eventually-consistent and cache-served; the booking path favors consistency over availability (better to say "try again" than to double-sell).
- Latency: a hold decision returns in tens of ms so the seat map feels live.
- Fairness: during a flash sale, earlier arrivals get first shot (waiting-room FCFS).
Back-of-envelope estimation (BOTE)
- Steady-state writes: 10M tickets/month ÷ (30 × 86,400 s) ≈ ~4 bookings/s average. Trivial on paper.
- Steady-state reads: ~3B seat-map/browse views/month ÷ 2.6M s ≈ ~1,150 reads/s average — a ~300:1 read:write ratio. The browse path is cache-and-replica territory; the write path is where correctness lives.
- The number that actually matters — peak concentration on one hot event: 50K buyers, each attempting a hold within the first ~10 s of the on-sale → ~5,000 hold attempts/s aimed at a SINGLE event's seat rows. Compare to the 4 writes/s system average: that's ~1,250× the mean load concentrated on one partition. This single line is why "shard by event and you're done" is wrong (see Break It #3).
- Waiting-room queue size: 50K buyers queued; if the booking service safely serializes ~1,000 admits/s for this event, the room drains in 50K ÷ 1,000 = ~50 s. Buyers see a live "you are #34,102 in line, ~34 s" position.
- Storage: a stadium ~= 50-70K seats; even a large venue's full seat inventory is a few MB. A season of events is single-digit GB. Storage is a non-issue — contention, not capacity, is the whole game.
API sketch
POST /events/{eventId}/holds
body: { seatIds: [..], userId, waitingRoomToken }
-> 201 { holdId, heldSeats, holdExpiresAt } // atomic; all-or-nothing
-> 409 SEAT_UNAVAILABLE { conflictingSeatIds } // someone else won
-> 429 NOT_ADMITTED // waiting-room token not yet admitted
POST /holds/{holdId}/confirm
header: Idempotency-Key: <uuid>
body: { paymentToken }
-> 200 { bookingId, seats } // hold re-checked valid, then BOOKED
-> 410 HOLD_EXPIRED // TTL lapsed before payment landed (refund if charged)
DELETE /holds/{holdId} -> 204 // explicit release, seats back to AVAILABLE
GET /events/{eventId}/seatmap -> { seats:[{seatId,status}] } // cache/replica, eventually-consistent
Data model & partitioning
seatsrow per seat:seat_id (PK), event_id, section, status ENUM(AVAILABLE|HELD|BOOKED), held_by, hold_expires_at, version, booking_id. The seat row IS the source of truth — the hold is astatus + hold_expires_atpair on this row, not a lock held open across the payment window.- Partition by
event_id. All of one event's seats live together, so a seat-map read is a single-partition scan and a hold is a single-row write. The catch, and the interview trap: a hot event = one hot partition. Sharding by event does not spread a flash sale's load — all 5,000 holds/s hit the one node that owns that event. Mitigations (Optimise): per-seat (not per-section) locking to maximize intra-event parallelism, and if a single event still saturates one node, sub-shard that event's seats across nodes bysection. - Browse/seat-map served from read replicas + a short-TTL cache; it's allowed to lag the truth by a second (a seat may show "available" then 409 on hold — that's correct, the atomic hold is the arbiter, not the map).
- Idempotency table keyed by
Idempotency-Keyforconfirmso a retried payment confirmation can't double-book or double-charge.
High-level diagram
The waiting room meters the stampede down to a safe admit rate; the booking service owns the state machine and makes the AVAILABLE→HELD transition atomic against the seat-hold store; a background expiry worker recycles abandoned holds; payment sits after the fast hold and re-checks the hold's validity before flipping HELD→BOOKED.
Rubric — what a strong answer hits
- Identifies check-then-book as a race and makes the AVAILABLE→HELD transition atomic (conditional UPDATE / SELECT FOR UPDATE), rather than hand-waving "add a lock."
- Models the seat as a 3-state machine with a self-expiring HELD state and a background reaper — not a boolean.
- States the BOTE out loud and calls out the hot-partition concentration (5,000 holds/s on one event vs 4/s average) as the real scaling problem, not raw storage.
- Separates the strongly-consistent booking path from the eventually-consistent browse path and justifies each.
- Puts a virtual waiting room in front for fairness + load shedding, and explains it protects availability while the atomic hold protects correctness.
- Handles the hold-expiry-vs-payment race explicitly (confirm re-checks the hold; 5s safety buffer; idempotent confirm).
- Model-answer reading: check your worksheet against the guide's Designing Ticketmaster and the traced deep-dive Designing Ticketmaster — Seat Reservation Without Double-Booking, Traced. The atomic-hold mechanism is the same pattern as Dispatch & Assignment: the double-booking race.
5. Break it — three traced failures
Failure 1 — the check-then-act double-booking race, exact interleaving. Two buyers, one seat 5A, naive read-then-write:
time Buyer A Buyer B seat 5A
---- ------------------------------ ------------------------------ -----------
t0 SELECT status → 'AVAILABLE' AVAILABLE
t1 SELECT status → 'AVAILABLE' AVAILABLE
t2 if(AVAILABLE) → true AVAILABLE
t3 if(AVAILABLE) → true AVAILABLE
t4 UPDATE status='BOOKED', A BOOKED (A)
t5 UPDATE status='BOOKED', B BOOKED (B) ← A silently lost
t6 return "you got it!" return "you got it!" ONE seat, TWO barcodes
Fix: collapse read+write into one atomic conditional statement — UPDATE seats SET status='HELD',held_by=? WHERE seat_id=? AND status='AVAILABLE'. The row's write lock forces t4 and t5 to serialize and re-evaluate the predicate; B's UPDATE now matches 0 rows and B is told "sold." This is the break-it test: run 1,000 threads holding the same seat — with the naive version, assert you see >1 "success"; that failing assertion (success count > 1) IS the lesson. With the conditional UPDATE, exactly 1 thread gets affected_rows=1 and 999 get 0.
Failure 2 — hold expiry races payment completion. Payment is in flight while the TTL lapses:
t=4:58 Buyer A submits payment (hold expires at 5:00)
t=5:00 Expiry Worker: hold_expires_at ≤ now → seat 5A HELD→AVAILABLE, resold to Buyer C as HELD
t=5:01 A's payment succeeds → naive confirm blindly sets 5A BOOKED to A
→ A is charged AND C holds/books 5A. Double-sold again, plus a chargeback.
Fix, two parts: (1) confirmBooking must be a conditional transaction: UPDATE seats SET status='BOOKED',booking_id=? WHERE seat_id=? AND held_by=A AND status='HELD' AND hold_expires_at > now(). If A's hold already expired/was reassigned, this matches 0 rows → reject the confirm and refund A's charge (payment is reversible; a double-booked seat is not). (2) Give the server-side TTL a ~5-second safety buffer beyond the client's countdown, so the worker never reclaims a seat while the buyer's own timer still shows time left. Idempotency-Key on confirm makes a retried "success" safe.
Failure 3 — one hot event = one hot partition, DB melts. You sharded by event_id, felt clever, and the Taylor Swift on-sale sends all 5,000 holds/s to the single node owning that event. Even with correct atomic holds, contending transactions queue on row/page locks; each waiting transaction pins a connection; the pool (say 200 connections) drains in milliseconds; new requests can't even get a connection and time out — the whole event freezes, including buyers who would have succeeded. Correctness held, availability collapsed.
Fix: (1) the virtual waiting room meters admits to a rate the partition can sustain (~1,000/s), turning a 5,000/s stampede into a steady drain. (2) Lock at per-seat granularity, never per-section/per-event, so non-conflicting buyers proceed in parallel. (3) If one event still saturates one node, sub-shard that event's seats by section across nodes so the load fans out. Sharding by event alone is necessary but not sufficient for a flash sale — that's the trap.
6. Optimise — with trade-offs vs named alternatives
Every row is "buys X at cost Y, use when Z, not W." Defend each in the room.
| Decision | Option A | Option B | Pick when |
|---|---|---|---|
| Atomic seat-hold mechanism | Optimistic conditional/CAS UPDATE (WHERE status='AVAILABLE', check affected_rows) |
Pessimistic SELECT FOR UPDATE (explicit row lock, then update) |
CAS for a single-seat hold — one round trip, no lock held across app logic, and contention on a given seat is naturally low (only the handful racing that exact seat). Reach for SELECT FOR UPDATE when the hold decision spans multiple rows atomically ("4 adjacent seats or none") or you need to read-then-decide within one transaction. A third option, a distributed lock (Redis/Redlock), is fast and offloads the DB but adds a second source of truth with murky failover — use it only for the ephemeral hold, never as the sole guardian of the committed sale. |
| When to charge the card | Hold-then-pay (fast atomic hold; payment after, off the lock) | Pay-immediately (charge inside the hold transaction) | Hold-then-pay almost always — the seat lock is held for milliseconds, not the seconds a payment gateway takes, so the hot partition isn't pinned by slow external I/O. It costs you the expiry-vs-payment race (Break It #2), which a conditional confirm + safety buffer solves. Pay-immediately only for ultra-low-contention GA sales where holding a lock through payment is harmless and you want zero abandoned-cart complexity. |
| Flash-crowd load control | Virtual waiting room (FCFS queue, metered admit) | Rate-limit / let-it-fail (429 the overflow, retry) | Waiting room when the event is a marquee on-sale where fairness and UX matter (a visible "#34,102 in line" beats random 429s) and you must protect the DB from a stampede. Plain rate-limiting/shedding is fine for steady traffic or lower-stakes events where you don't owe buyers an ordered, fair queue — it's simpler but feels arbitrary and re-stampedes on retry. |
| Lock granularity | Per-seat lock | Per-section (or per-event) lock | Per-seat — maximizes parallelism: two buyers wanting different seats never block each other, which is exactly what you need when thousands buy simultaneously. Per-section locking is simpler and avoids some deadlock ordering concerns for multi-seat holds, but it serializes an entire section behind one buyer — acceptable only for tiny venues or low concurrency, never for a stadium flash sale. |
| Seat-map (browse) consistency | Eventually-consistent cache/replica | Strongly-consistent read from primary | Eventually-consistent for the map — it's read 300× more than it's written, and a seat briefly showing "available" then 409-ing on hold is correct behavior (the atomic hold is the arbiter). Strong reads from primary only if you can't tolerate any stale display, which needlessly loads the primary and buys little — the map is a hint, the hold is the truth. |
7. Defend under drilling
Q1 — "How, exactly, do you prevent double-booking? Walk me through the one line that guarantees it."
A single atomic conditional write: UPDATE seats SET status='HELD', held_by=? WHERE seat_id=? AND status='AVAILABLE'. The database takes a row write-lock and re-evaluates status='AVAILABLE' at write time, so of N concurrent buyers racing that seat, exactly one transaction finds the predicate true and gets affected_rows=1; the rest get 0 and are told "sold." There is no read-then-write gap to exploit because the check and the mutation are the same statement. That's the guarantee — not a lock I remembered to take, but a predicate the DB enforces atomically.
Q2 — "What happens if payment succeeds but the hold already expired?"
This is the nastiest race (Break It #2). confirmBooking is itself conditional: ... SET status='BOOKED' WHERE seat_id=? AND held_by=? AND status='HELD' AND hold_expires_at > now(). If the hold lapsed and the seat was reassigned, this matches 0 rows → I reject the booking and refund the charge. The invariant: a payment is reversible, a double-booked seat is not, so on conflict I always unwind the money, never the seat. I also add a ~5-second server-side safety buffer so the reaper never reclaims a seat while the buyer's own countdown still shows time, and an Idempotency-Key so a retried confirm can't double-charge or double-book.
Q3 — "Optimistic CAS or pessimistic SELECT FOR UPDATE — which, and why?"
CAS for the common single-seat hold: one round trip, no lock held across application logic, and per-seat contention is genuinely low (only the few racing that exact seat). I switch to SELECT FOR UPDATE when a hold must span multiple rows atomically — "these 4 adjacent seats or none" — where I need to lock all of them, verify, then update in one transaction. Both give the same no-double-book guarantee; the choice is about transaction shape, not correctness. What I would not do is make Redis the sole arbiter of the committed sale — its failover can lose a lock and surface two holders.
Q4 — "Why a waiting room? Can't you just autoscale the booking service?"
Autoscaling app servers doesn't help, because the bottleneck isn't CPU — it's the single hot partition owning that event's seat rows. More app servers just means more connections contending on the same rows and draining the pool faster (Break It #3). The waiting room attacks the actual constraint: it meters admits to the rate that one partition can safely serialize, and gives fairness (FCFS) as a bonus. It protects availability; the atomic hold protects correctness. Different problems, different tools.
Q5 — "Seat map shows a seat available, but the hold 409s. Is that a bug?"
No, that's correct by design. The seat map is an eventually-consistent, cache/replica-served hint (read 300× more than written); the atomic hold is the single arbiter of truth. Making the map strongly consistent would hammer the primary for a display that's stale the instant it's rendered anyway. The right UX is optimistic display + authoritative hold: show the map fast, and let the 409 gracefully tell the late buyer "just taken, here are others."
The 100× escalation — "Taylor Swift Eras on-sale: not 50K but 2M+ in the queue for one event. What breaks first, and what do you change?"
At 2M concurrent the single-partition ceiling is the wall. Three moves, in order: (1) The waiting room becomes load-bearing infrastructure, not a nicety — it absorbs all 2M, hands out FCFS admission tokens, and drips buyers to booking at the partition's safe rate; queue depth and wait time (~2M ÷ 1,000/s ≈ 33 min) are shown honestly. (2) Sub-shard the single event's seats across many nodes by section so the hold load fans out horizontally instead of pounding one node — the whole point being that "shard by event" is not enough when one event is the load. (3) Push more work off the synchronous path: holds stay synchronous and atomic (correctness can't be deferred), but seat-map updates, analytics, and even payment capture go async behind queues so a payment-gateway slowdown can't back-pressure into seat contention. What I explicitly do not relax is the atomic hold — correctness is the one thing that must not bend at 100×; everything else (latency, fairness granularity, map freshness) is negotiable.
Q6 — "A buyer holds seats then vanishes. How do those seats come back, and how fast?"
The HELD row carries hold_expires_at = now()+5min. A background Expiry Worker scans holds where hold_expires_at ≤ now and flips them HELD→AVAILABLE (and signals the waiting room to promote the next buyer). Because every hold uses the same fixed TTL, insertion order equals expiry order, so the sweep is an ordered scan, not a full-table search. Worst-case a seat is stranded for 5 minutes + one sweep interval — tune the TTL down for hotter events to recycle inventory faster, at the cost of buyers who type their card slowly.
8. You can now defend
- You can quantify why check-then-book fails — deriving ~1,000 double-sold seats from a 10 ms race window × 50K buyers, not hand-waving "there's a race."
- You can make a seat-hold atomic and name the exact mechanism (conditional CAS UPDATE vs SELECT FOR UPDATE vs distributed lock) and when each applies — the crux of the entire problem.
- You can model the seat as a self-expiring 3-state machine and defend the hold-expiry-vs-payment race with a conditional confirm + safety buffer + idempotency.
- You can separate correctness (atomic hold) from availability (virtual waiting room, per-seat locking, section sub-sharding), and explain why "shard by event" alone doesn't survive a flash crowd.
- You can escalate to a 100× Taylor Swift on-sale and say precisely what bends (latency, map freshness) and what must not (the atomic hold).
Re-authored/Deepened for this guide. Model-answer reading: Designing Ticketmaster and Designing Ticketmaster — Seat Reservation Without Double-Booking, Traced (System Design Problems). Concept pages: MVCC & Locking — row locks, SELECT FOR UPDATE, deadlocks (optimistic vs pessimistic); What Is Distributed Locking; Idempotency Keys for Payments; and Dispatch & Assignment: the double-booking race.
🤖 Don't fully get this? Learn it with Claude
Stuck on Design Ticketmaster? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Design Ticketmaster** (Hands-On Builds) and want to truly understand it. Explain Design Ticketmaster from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Design Ticketmaster** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Design Ticketmaster** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Design Ticketmaster** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.