Worked problems
Ticketmaster
Sell a finite set of seats without selling the same seat twice, including the spike when a show goes on sale.
What you are designing
People browse events and buy specific seats. A seat has one buyer. Popular shows go from idle to a huge spike in a second. Assume events are known ahead of time, a large show has tens of thousands of seats, and checkout may call a card network that takes hundreds of milliseconds.
Requirements
- Search events by city, date, and artist.
- Show a seat map that is fresh enough that two people are not both sent to pay for a seat that is gone, within a second or two.
- Hold a seat, pay, and confirm, without double-selling.
- Release holds that are abandoned.
Double-selling is worse than telling someone the seat is gone. Fail closed.
Entities and API
Event, Seat(id, event_id, status, holder, lease_until), Order, Payment.
GET /v1/events?city&date
GET /v1/events/{id}/seats
POST /v1/events/{id}/holds { seat_ids } -> hold or 409
POST /v1/orders/{id}/pay Idempotency-Key
The seat map GET is allowed to be a second stale. The hold is the truth: 201 with a lease deadline, or 409 if any requested seat is not open. Do not hold a subset and return success unless the client asked for a partial. Pay uses an idempotency key and returns 200 only when the seat is sold and the charge is captured, or explicitly authorized if you capture later. A timeout on pay leaves the order unknown. The client retries the same key. It does not start a second hold.
High-level design
flowchart LR
U[Buyer] --> Edge[Waiting room]
Edge --> API
API --> Search[(Event search)]
API --> DB[(Seat rows)]
API --> Pay[Payments]
Sweep[Sweeper] --> DB
Event search is a replica or a search index. It can be a few seconds stale. The seat map and the hold hit the primary for that event. Payment is an external call bracketed by the hold, not inside the database transaction.
Data model
CREATE TABLE seats (
id BIGINT PRIMARY KEY,
event_id BIGINT NOT NULL,
label TEXT NOT NULL,
status TEXT NOT NULL, -- open | held | sold
holder_id BIGINT,
lease_until TIMESTAMPTZ,
version INT NOT NULL DEFAULT 0
);
CREATE INDEX seats_event_status ON seats (event_id, status);
UPDATE seats
SET status = 'held', holder_id = :user, lease_until = now() + interval '5 minutes', version = version + 1
WHERE id = :id AND event_id = :event AND status = 'open';
Hold a small set in one transaction so you do not leave the user with one of three seats. If any update matches zero rows, roll back and return 409.
Deep dives
The spike. Put a waiting room in front of the on-sale: a queue token, admitted at a configured rate, with a per-user cap so a botnet cannot drain the room. The seat API rejects requests without a valid admit token. Isolate a famous event on its own partition if its write rate would disturb everyone else.
Payment saga. On successful hold, create an order in pending and call the processor with an idempotency key. On success, conditional-update seats from held by this holder to sold. On failure, set them back to open if you still hold them. If the process crashes after the charge and before sold, the saga resumes from the order row and either finishes the assignment or refunds. Do not hold a FOR UPDATE lock during the card call.
Fairness and bots. Captchas and identity checks belong at admission, not as a hope. Limits per payment instrument and per account are part of the design. Say that a perfect bot solution does not exist, and that the hard guarantee is still one seat, one buyer.
Showing the map. Serve seat status from a replica or a short-TTL cache for browsing. The hold path ignores that cache and uses the conditional update as the truth. The UI must tolerate “the map said open and the hold said no.”
Levels
- Mid-level. Conditional seat update, a hold, a payment call outside the transaction, a basic API.
- Senior. Waiting room, expiry sweeper, idempotent pay, and a clear fail-closed stance. Knows search is a separate path.
- Staff. Hot-event isolation, compensation when charge and seat assignment diverge, bot policy, and what you tell the user in each failure.
Further reading
Check yourself
Answer the questions. Multiple choice is scored on this device only.
1. The statement that sells a seat safely is:
2. Why a waiting room on a popular on-sale?
3. What happens when a hold expires?