Ticketmaster
The hard part of a ticket sale is scarce inventory under a burst, not drawing a payment box.
Interview scope and guarantees
Browse events, view approximate seat availability, hold specific seats, and buy the held seats. The invariant is that one seat has at most one completed sale. Browsing may be stale, but holding must serialize conflicting buyers. Agree a hold duration and a clear response for expired holds.
Capacity worksheet
Assume 100,000 buyers arrive in one minute for 10,000 seats: roughly 1,667 admissions/s average before retries. If each user refreshes 5 times, reads reach 8,333/s. The contention is concentrated on one event, so adding database shards for other events does not repair the hot event.
Concrete API contract
POST /events/{id}/holds {seatIds} -> {holdId,expiresAt}
POST /holds/{id}/purchase {paymentToken} -> 202 {orderId}
GET /orders/{id} -> {state}
Use a stable purchase key across retries.Data model and access paths
seats(event_id, seat_id, state, hold_id, expires_at, version); PK(event_id,seat_id)
holds(hold_id PK, owner_id, state, expires_at)
orders(order_id PK, hold_id UNIQUE, payment_ref UNIQUE, state)Evolve a solution and explain each change
Figure — Three architecture decisions for Ticketmaster, including the pressure each introduces.
Step 1: Model one seat
Give every seat an authoritative ownership row and conditional state transitions. Checking availability before writing permits two winners.
Step 2: Hold before payment
Reserve with a hold ID and deadline; confirmation checks that same hold. Late payment can arrive after expiration and reassignment.
Step 3: Control the admission rate
Use a waiting room and bounded checkout traffic while retaining the database decision. A queue or cache ticket does not itself own a seat.
Responsibility overview
Figure — Connected responsibilities for Ticketmaster. Trace the authoritative and derived paths separately.
Worked end-to-end scenario
Two buyers select seat A12. The first transaction changes available to held with hold h1; the second receives unavailable. Buyer one starts payment, but h1 expires before confirmation. A later buyer holds the seat as h2. When h1 receives a successful payment callback, its confirmation must check current hold identity and state. It cannot convert h2 to sold. Record the late payment and reconcile by refund or another business-approved outcome. A browser countdown is useful feedback, but the server deadline is authoritative. Repeating the callback returns the original reconciliation result rather than issuing multiple refunds.
Why these access paths matter
Use event-plus-seat as the ownership key and hold ID as a confirmation condition. Index pending holds by expiry for bounded cleanup. The event availability projection can lag, but checkout must use the authoritative row. A single popular event can dominate traffic; shard independent seats only when the cart contract permits multi-seat reservations across that boundary.
Build the baseline first
- Buyer → hold API → seat transaction.
- Hold owner → purchase workflow → payment provider.
- Verified payment → seat sale commit → receipt.
Evolve the design under load
- Waiting room → bounded admissions → hold API.
- Seat projection → cache → browsing clients.
- Expiry reconciler → conditional release of old holds.
Defend the hardest decision
For adjacent seats, acquire locks in a stable seat order and commit all requested seats or none. An atomic conditional update checks available state or a safely expired hold. Expiry is validated with authoritative time during acquisition; a background sweeper is cleanup, not the correctness mechanism. Purchase must verify ownership, hold state, and the permitted payment deadline. A distributed cache lock alone cannot guarantee no double sale after failover.
Failure and recovery analysis
A payment succeeds after the hold has expired and been reallocated. Do not reclaim the seat from the new buyer. Persist the late payment as a reconciliation case and refund or offer alternatives. To reduce this window, define a bounded payment-in-progress extension before contacting the provider. Never hold a database transaction open across a provider network call.
Security and privacy boundary
Authenticate hold owners, limit bot traffic and hold creation, and keep payment credentials at the payment provider.
Interview follow-ups with reasoning
Question: How do you prevent a waiting-room bypass?
Show answer and explanation
Answer: Require a signed admission token validated on every purchase path.
Question: How do you sell a whole group atomically?
Show answer and explanation
Answer: Use a single authoritative transaction where possible and define all-or-none behavior.
Question: How do you test correctness?
Show answer and explanation
Answer: Race concurrent holds and assert unique sale ownership under retries and expiry.
Operate and verify the design
Hold conflict rate, admission wait, expired-payment cases and stock reconciliation.
Race two buyers for the final seat while delaying the payment callback past hold expiry.
A second scenario to test transfer
Alice holds seat A7 until 12:05. Her payment response is delayed. At 12:06, Bob acquires a new hold after expiry. Alice’s success callback arrives at 12:07. The callback must not mark A7 sold to Alice merely because it says payment succeeded. It must compare the original hold identity. The safe result is a reconciliation/compensation path for Alice, while Bob’s hold remains intact.
A successful payment webhook contains an old hold token. What must the confirmation handler do?
Show answer and explanation
Answer: Authenticate and deduplicate the webhook, compare the current hold and payment state, and refuse to transfer inventory from a newer owner. Route the old payment into void/refund/reconciliation as appropriate; do not blindly mark sold.
Compare alternatives
| Mechanism | Role | Limitation |
|---|---|---|
| Waiting room | Bound load and define fairness | Does not reserve inventory |
| Atomic hold | Protect seat ownership | Conflicts remain normal |
| Hold expiry | Release abandoned inventory | Late external effects need handling |
| Reconciliation | Repair payment mismatch | Requires audit and operations |
Figure — Reservation state changes and the separate reconciliation path for a late payment.
A design-changing exercise
A payment succeeds one second after a seat hold expires. Should it always confirm?
Show answer and explanation
Answer: No. Confirm only the still-valid matching hold under the business policy. If ownership moved, preserve the new owner and reconcile the payment.
Design workshop: atomic multi-seat checkout
Choose reserved seating as the core scope: browse an event, hold adjacent seats, pay and obtain tickets. General admission is an extension with a conditional quantity counter instead of per-seat rows. Assume a five-minute hold, a two-second hold API target and zero double-sold seats. Browsing may be stale; the hold decision is authoritative.
The hold operation records customer/request fingerprint and a hold ID before returning. An identical retry returns the original seat set and expiry; changing seats under the same key returns 409. Read availability is not a reservation. Lock selected seats in event/seat order, check all of them, then update all or none.
BEGIN;
SELECT seat_id,state,hold_id,expires_at
FROM seats WHERE event_id=:event AND seat_id=ANY(:seat_ids)
ORDER BY seat_id FOR UPDATE;
-- Verify exactly the requested count and every row is eligible.
-- Expired holds are eligible only under the chosen ownership rule.
UPDATE seats SET state='held',hold_id=:hold,expires_at=:deadline
WHERE event_id=:event AND seat_id=ANY(:seat_ids);
INSERT INTO holds VALUES (:hold,:customer,:request_hash,:deadline,'active');
-- Persist request result and payment outbox atomically.
COMMIT;Add a unique request map, an expiry index on held seats/holds, and payment_events(provider,event_id) for callback deduplication. The payment worker runs outside this transaction. A successful callback confirms only rows still owned by this hold and a valid confirmation policy. If payment confirmation is allowed after the deadline, define an explicit grace state that prevents reassignment; otherwise refund/void late success. Do not casually mix those policies.
Figure — Two seats succeed or neither does.
The waiting room issues a signed token containing event, customer, admission ID and expiry. A server-side admission ledger enforces one use and records the resulting hold request; the signature alone cannot stop replay. FIFO order is a scenario choice, with randomized admission before opening only if explicitly disclosed. Admit based on measured hold capacity and recent contention, not just queue length. Client refreshes show queue progress without repeatedly querying the seat table.
Figure — Seat authority versus general-admission stock.
At 1,670 admitted requests/s buying four seats, expect roughly 6,680 seat-row updates/s plus requests and payment records. A hot seat's serial critical section, not total database IOPS, bounds contention. If a hold transaction spends 10 ms on the locked authority, a single contested seat can process at most roughly 100 serial decisions/s before waiting. Shed or queue requests rather than open unlimited database connections.
Exercise: A holds two seats, its payment succeeds after expiry, and B now holds one of them. Can A receive the other seat?
Show answer and explanation
Answer: No under an all-or-none two-seat purchase contract. Mark the old payment for compensation and preserve B's ownership. Partial fulfillment is a different product contract requiring explicit customer consent and accounting.