LearnastraTHE DESIGN COURSE

Design complete systems / CHAPTER 62

Design Ticketmaster-style seat booking

Let many buyers browse seat maps while preventing conflicting seat claims and retaining payment results needed after a timeout.

Advanced3h 20m of deliberate study10 questions

Builds on SQL versus NoSQL data stores, Strong versus eventual consistency, Synchronous versus asynchronous communication.

By the end, you can
  • Model the difference between an available seat, a temporary hold and a confirmed booking.
  • Claim all requested seats in one transaction without giving the same event-seat to competing active buyers.
  • Recover lost replies and unknown payments using the original provider operation ID and saved booking state.
INTERVIEW STEP 1 / 7Clarify the requirements

01Functional requirements

Browsing a seat map and owning a seat are different actions. The map can be a little old because many people read it at once; a hold must check current inventory before promising seats. Payment adds a second source of uncertainty because a provider can charge successfully even when its reply is lost.

  1. FR1: Event discovery. Search events by city, date, and performer and retrieve the selected event’s details.

  2. FR2: Catalog administration. Let authorized owners manage event and venue records, including the seat layout used for each event.

  3. FR3: Seat browsing. Show an approximate availability map with its timestamp or version. Seeing an available seat does not reserve it or authorize checkout; a hold request must still claim it.

  4. FR4: Multi-seat holds. Acquire all requested seats within one event or acquire none. Return a stable hold identity and expiry; repeating the same request must recover that result rather than extend the deadline.

  5. FR5: Checkout. Start a recoverable booking and payment process from a valid hold. Use the price saved by the server and retain operation IDs so retries recover the same booking and payment attempt.

  6. FR6: Booking retrieval. Let the authenticated buyer retrieve the current booking and honest pending, confirmed, cancelled, or refund-required state after a timeout or reconnect.

  7. FR7: Cancellation and refunds. Apply an explicit policy to cancellation of a confirmed booking, persist refund intent, and release inventory only according to the chosen refund and resale rules.

Out of scope

  • Resale and dynamic pricing.
  • Atomic purchases spanning several events. This exercise’s all-or-nothing seat group belongs to one event.

Example. Mina requests A7 and A8 for E1 while Leo requests A7. Both may have seen green seats, but only one valid claim of A7 can commit. We must also recover a lost hold response and a payment that succeeds after its shopping hold expires.

02Nonfunctional requirements

These are independently chosen exercise assumptions to validate, not measured production guarantees.

  1. NFR1: Hold-creation latency. At least 95% of measured hold requests must finish within 500 ms under the admitted, tested load. Include the complete claim transaction and saved retry result; measuring only the cached map response would miss the important work.

  2. NFR2: Hold duration. The shopping hold expires two minutes after creation, at the original deadline saved by the inventory service. Matching retries return that same expiry. Delayed cleanup does not extend it; later claims check fresh server-controlled time against the saved deadline.

  3. NFR3: Browse capacity. Plan for 100,000 browse requests/second at the assumed launch peak. Serve approximate maps separately from the writer and test response payload and cache capacity.

  4. NFR4: Admitted claim capacity. Admit at most the exercise’s tested 2,000 hold attempts/second to the inventory authority. Retest the hot-seat distribution and contention; a waiting room bounds work but cannot establish seat ownership.

  5. NFR5: Exclusive seat ownership. A seat for one event must belong to at most one active hold or booking at a time, and at most one confirmed buyer. The claim transaction checks current state and time, then commits all requested seats together or none. Converting a hold to its booking preserves that same exclusive ownership.

  6. NFR6: Uncertain-payment recovery. Every uncertain payment must retain the original provider operation ID and saved progress so the service can query or retry that same operation. When it is unknown whether the provider collected money, do not resell the booking’s payment-pending seats. If a late successful payment has no valid inventory claim, persist a corrective action such as a refund instead of assigning seats already owned elsewhere.

INTERVIEW STEP 2 / 7Estimate the scale

03Back-of-the-envelope Estimates

Separate people looking at a map from people trying to change seat ownership. Both matter, but they stress different code and storage.

  1. Browse delivery. At 100,000 responses/second and 50 KB per map, payload is 100,000 ×50,000 =5 GB/second before transport overhead. Serve approximate maps through caches/CDN or smaller updates, away from the seat writer.
  2. Admitted holds. Assume testing shows the inventory database safely handles 2,000 hold attempts/second for the intended seat-contention pattern. A waiting room must limit work reaching that writer. Adding web processes cannot make the same A7 seat independently claimable.
  3. Waiting time. With 100,000 attempts ahead and 2,000 admitted/second, the idealized wait is at least 100,000 ÷2,000 =50 seconds. Abandonment, stalled attempts and fairness can change it; do not promise an exact countdown from this division.
  4. Seat records. A 50,000-seat event at 200 bytes per state record needs 50,000 ×200 =10 MB raw. The launch problem is many buyers competing for that small table, not merely its size. A million events would create a separate catalog-capacity problem.
  5. Rows touched. At 2,000 holds/second with two seats each, the writer checks about 4,000 seat rows/second before conflicts and retries. Everyone choosing the same row is harder than evenly spread requests, so test that hot pattern.
  6. Pending payments. Approximate simultaneous payment work as admitted checkouts/second ×mean provider delay in seconds. Size workers and reconciliation queues for those unresolved operations. An unlimited queue creates more waiting, not more than 50,000 seats.
INTERVIEW STEP 3 / 7Define the API

04API Design and Retry Contracts

Mina selects seats A7 and A8 for event E1. Her app sends a key for that one intended hold; H1 is the hold record returned after both claims are saved.

POST /events/E1/holds
Idempotency-Key: mina-hold-17
Content-Type: application/json
{"seatIds":["A7","A8"]}
{"holdId":"H1","seatIds":["A7","A8"],"expiresAt":"2026-10-24T13:02:00Z","state":"held"}
  1. Discover and browse. GET /events?city=…&date=…&performer=… searches the catalog. GET /events/E1/seats returns a map with a timestamp/version. It helps choose seats but grants no ownership.
  2. Claim a group. POST /events/E1/holds takes seat IDs and an idempotency key. The server derives the buyer from login. Return 201 with the complete group and original expiry, or 409 if the full group cannot be acquired. The example starts at 13:00 UTC and expires at 13:02 UTC.
  3. Repeat the same intent. Save the buyer, key, original seat parameters and hold result together. A matching retry returns H1. Reusing the key with different seats returns a conflict rather than silently changing or extending H1.
  4. Start and recover checkout. POST /holds/H1/checkout uses another stable operation key and returns booking B1 with pending or confirmed state. GET /bookings/B1 tells its authenticated buyer the current outcome after a timeout. Unknown payment is not a confirmed failure.
  5. Administer and cancel. Authorized owners use POST /events or version-checked PATCH /events/E1 for the catalog. POST /bookings/B1/cancel checks buyer ownership, current version and the stable cancellation key. Return honest refund/pending state; accepting cancellation does not prove a provider completed a refund.
INTERVIEW STEP 4 / 7Define the data model

05Data Model and Access Paths

The inventory database is responsible for accepted seat writes. A cached map is only a copy for browsing. Identify a seat by event and seat ID, because A7 at tomorrow’s show is different from A7 tonight.

Record / key Fields and access pattern Responsibility
Event / venue Performer, city/date, venue and seat layout Catalog search and static map
Event seat (eventId,seatId) State, holdId, expiresAt, bookingId, version Authoritative inventory
Hold holdId Account, event, seat list, expiry, state Fixed-duration temporary claim
Booking bookingId Hold, authoritative price/currency, state Product purchase workflow
Payment operationId Booking, provider reference, state Correlate external outcomes
Idempotency / outbox / inbox Request hash/result; transition/event IDs Safe retries and durable work
  • The hold handler changes all requested event-seat rows and creates H1 with one fixed deadline. Its request-result record lets a lost reply recover H1.
  • The booking handler creates B1, saves the server’s price/currency and tracks whether checkout is pending, confirmed or needs cancellation/refund. The browser cannot choose a charge amount.
  • The payment worker uses a saved operation ID when asking the provider for authorization, capture or status. Authorization approves/reserves the amount; capture collects the money when the method supports these separate steps.
  • An outbox row retains work to send after a database commit, such as capturing an already reserved booking. A relay reads and retries that row if a process crashes before sending.
  • An inbox row saves a received provider event ID and payload. Processing checks that ID and booking state so a repeated callback does not repeat the purchase or move a confirmed booking backward.

Keep the short shopping hold separate from B1’s payment-pending inventory reservation. The latter must remain protected while a possibly successful capture is unresolved; H1’s old two-minute expiry cannot silently release it.

INTERVIEW STEP 5 / 7Draw the high-level design

06Baseline: Acquire seats in one transaction

First grant one complete seat group safely, using one inventory service and transactional database. Mina and Leo may both have seen A7 as available in the map.

  1. Mina’s hold handler verifies her account and reads her idempotency key. If it already has a result, compare the saved seat parameters and return that same H1 result, or conflict if the parameters changed.
  2. For a new request, begin a transaction and lock A7 then A8 in a consistent seat order. Other transactions choosing overlapping seats must wait or conflict rather than independently claim old state.
  3. After obtaining locks, read fresh server-controlled time and each row’s current owner/state. A seat qualifies only if available or covered by an eligible expired shopping hold. A payment-pending booking reservation is not such an expired hold.
  4. If either seat fails, roll back every tentative claim. Mina receives neither seat, preserving the all-or-nothing contract.
  5. Otherwise save H1, both seat claims, the two-minute deadline and Mina’s key-to-result record in the same transaction. Commit before returning 201.
  6. Leo’s A7 transaction then rechecks the row. If Mina’s H1 is still valid, it returns 409 instead of a second hold.

The time check must occur after a lock wait. In PostgreSQL, now()/CURRENT_TIMESTAMP retain the transaction-start time; a transaction waiting past a deadline could therefore test an old time. Use a fresh value such as clock_timestamp() after locking, or an equivalent decision-time mechanism.

A cleanup worker can remove expired holds and refresh maps later. It does not extend validity: every claim checks the saved deadline itself. Likewise, a conditional update can replace explicit locks only if a transaction still protects the complete seat group. Never keep seat locks while waiting for a payment-provider network call.

BUILD THE DESIGN, ONE CHANGE AT A TIME

Watch the architecture evolve.

Advance through the stages in this same diagram. Existing components keep their places; additions fill the available space, then add rows. Highlights show what changed from the previous stage, and numbered arrows match the connection key below.

Stage 1 of 3

RetainedAdded / new routeChanged responsibility / route

Start here. First prove that Mina’s A7/A8 request and Leo’s A7 request cannot commit conflicting or partial groups.

Atomic seat holds

Atomic seat holdsMina’s app selects seats from an approximate map. The writer locks and checks the complete selected group in a consistent order. It commits H1 and the saved retry result; a matching retry returns the same deadline. Numbered arrows correspond to the connection key below. All connections are directional.BuyerStable hold/checkoutidentityInventory serviceShort all-or-nothinghold transactionInventorydatabaseEvent-seat rows,holds, idempotencyresultCatalog + mapsourceEvent/venue search;approximateavailability123Atomic seat holdsMina’s app selects seats from an approximate map. The writer locks and checks the complete selected group in a consistent order. It commits H1 and the saved retry result; a matching retry returns the same deadline. Numbered arrows correspond to the connection key below. All connections are directional.BuyerStable hold/checkoutidentityInventory serviceShort all-or-nothinghold transactionInventorydatabaseEvent-seat rows,holds, idempotencyresultCatalog + mapsourceEvent/venue search;approximateavailability123Atomic seat holdsMina’s app selects seats from an approximate map. The writer locks and checks the complete selected group in a consistent order. It commits H1 and the saved retry result; a matching retry returns the same deadline. Numbered arrows correspond to the connection key below. All connections are directional.BuyerStable hold/checkoutidentityInventory serviceShort all-or-nothinghold transactionInventorydatabaseEvent-seat rows,holds, idempotencyresultCatalog + mapsourceEvent/venue search;approximateavailability123

Follow the numbered arrows

  1. Buyer → Catalog + map sourcebrowse event and approximate seats
  2. Buyer → Inventory serviceclaim A7/A8 with same request key
  3. Inventory service → Inventory databasefresh expiry; claim complete set or none
The map helps buyers choose. The inventory transaction reads current rows and grants the entire group or none before returning a hold.
Trace the diagram in words
  1. Mina’s app selects seats from an approximate map.
  2. The writer locks and checks the complete selected group in a consistent order.
  3. It commits H1 and the saved retry result; a matching retry returns the same deadline.
What this stage establishes
  • The hold handler uses event-seat IDs and a short database transaction.
  • It commits all claims, the fixed hold and saved request result together.
  • After a lock wait it checks expiry with fresh server-controlled time.

The cost: Competing buyers still wait or receive conflict. More web processes cannot eliminate competition for the same seat, even when the table is only 10 MB.

Read the reasoning for this stage →
Smallest working system
INTERVIEW STEP 6 / 7Go deep

07Evolution: Coordinate payment as a recoverable workflow

First measure whether a larger database improves memory or disk throughput. It cannot remove competition for the same A7 row. Separate reads, admitted claims and provider work as they grow.

Protect the inventory writer

  1. Catalog and map-serving code use caches or read copies to handle browse traffic. A stale green seat may cause a later hold conflict; it cannot create a valid claim.
  2. A waiting room sends only the tested attempt rate to the hold handler. Bind its tokens to accounts/sessions and expire them; admission is permission to try, not a seat reservation.
  3. Assign different events to separate inventory owners so unrelated shows do not share every write limit. One exceptionally hot event can still contend on its own seats. Do not use a lagging read replica as the final claim check.

Recover checkout with authorization then capture, when supported

  1. While H1 is valid, the checkout handler saves pending booking B1 and a stable payment operation. This saves work to resume; it does not extend H1 automatically.
  2. A payment worker asks the provider to authorize using that operation ID, outside seat locks. A retry keeps the same provider identity so it can recover an uncertain earlier request.
  3. After authorization, the booking handler locks/checks H1 again with fresh time. If it is still valid and belongs to the buyer, atomically convert its seats to B1’s exclusive payment-pending reservation. H1’s short deadline no longer governs those rows.
  4. Commit a pending capture task in the outbox. The worker sends capture with the stable payment identity and resolves its result through the provider’s supported retry/status rules.
  5. After established capture success, confirm B1. After definitive capture failure, record cancellation and release B1’s reservation. If capture is unknown, keep the reservation while checking the provider; a timeout cannot prove that money did not move.
  6. If the final hold check fails, void the authorization rather than capture unavailable seats. Some methods instead collect money immediately or asynchronously before a valid reservation is secured. A late success after H1 expired and seats were validly claimed elsewhere needs a saved refund/compensation process, not another seat owner.

This is a multi-step purchase across the inventory database and payment provider, often called a saga. The useful mechanism is saved states, stable external operations and corrective actions, not the label. Alert on old unresolved payments and define escalation so uncertainty does not disappear into an invisible queue.

Inventory ownership across checkout states

Inventory ownership across checkout statesThe hold transaction temporarily claims the complete group as H1. After authorization, the booking handler rechecks H1 and converts ownership to pending B1. Established capture success confirms B1; definitive failure can release it. A late charge made without inventory instead needs refund recovery. Numbered arrows correspond to the connection key below. All connections are directional.AvailableNo active ownerShopping hold H1Expires after twominutesB1payment-pendingExclusive reservationConfirmed B1Capture reconciledCancelled /releasedDefinitive failureonlyRefund-requiredLate pay; noinventory123456Inventory ownership across checkout statesThe hold transaction temporarily claims the complete group as H1. After authorization, the booking handler rechecks H1 and converts ownership to pending B1. Established capture success confirms B1; definitive failure can release it. A late charge made without inventory instead needs refund recovery. Numbered arrows correspond to the connection key below. All connections are directional.AvailableNo active ownerShopping hold H1Expires after twominutesB1payment-pendingExclusive reservationConfirmed B1Capture reconciledCancelled /releasedDefinitive failureonlyRefund-requiredLate pay; noinventory123456Inventory ownership across checkout statesThe hold transaction temporarily claims the complete group as H1. After authorization, the booking handler rechecks H1 and converts ownership to pending B1. Established capture success confirms B1; definitive failure can release it. A late charge made without inventory instead needs refund recovery. Numbered arrows correspond to the connection key below. All connections are directional.AvailableNo active ownerShopping hold H1Expires after twominutesB1payment-pendingExclusive reservationConfirmed B1Capture reconciledCancelled /releasedDefinitive failureonlyRefund-requiredLate pay; noinventory123456

Follow the numbered arrows

  1. Available → Shopping hold H1atomic complete seat claim
  2. Shopping hold H1 → B1 payment-pendingauthorize; recheck; commit
  3. B1 payment-pending → Confirmed B1capture confirmed
  4. B1 payment-pending → Cancelled / releasedcapture definitively failed
  5. Shopping hold H1 → Availableexpiry on claim path
  6. Shopping hold H1 → Refund-requiredlate captured payment
H1 expires as a short shopping hold. After a valid conversion, B1’s seats stay reserved while capture is unknown; that old hold deadline no longer authorizes resale.
Trace the diagram in words
  1. The hold transaction temporarily claims the complete group as H1.
  2. After authorization, the booking handler rechecks H1 and converts ownership to pending B1.
  3. Established capture success confirms B1; definitive failure can release it. A late charge made without inventory instead needs refund recovery.
See stage 2: Read split and admission ↗See stage 3: Recoverable payment workflow ↗
INTERVIEW STEP 7 / 7Find bottlenecks and failure points

08Failure and Recovery Scenarios

Follow the saved records through these separate failures:

  1. Lost hold reply. Mina repeats the same key and seat set. The hold handler returns H1’s original result and expiry. A new deadline on retry would let repeated calls hoard seats.
  2. One seat is unavailable. If A8 fails the claim check, the transaction rolls back A7 too. A later repair after charging would not implement this all-or-nothing hold.
  3. Lost payment reply. Store B1 as pending/uncertain and query or retry the original provider operation. A new charge ID could collect twice. Keep a converted payment-pending reservation protected until the money outcome is established.
  4. Repeated or old callback. Verify the provider signature, save the received event before claiming durable acceptance and check its event ID when processing. A repeated event must not repeat the transition. Also check current B1 state so an old pending event cannot overwrite confirmed.
  5. Crash between commit and send. An outbox row remains after the booking transaction, so another relay can send the capture task. If the provider succeeded but its callback was lost, the stored provider reference lets reconciliation query that same result.
  6. Money without seats. For a method that collected before valid reservation, record refund-required if the old hold expired and another buyer owns the seats. Show this honest state until the refund or chosen compensation finishes.
  7. Cancellation after confirmation. The cancellation handler records refund intent and follows the explicit refund/resale policy before releasing inventory. Expired-shopping-hold cleanup cannot cancel a paid booking.

Retain request results for the stated retry window. Neither a missing response nor a duplicate queue delivery tells the service which business state is currently true; it must read the appropriate saved record.

09Trade-offs and Validation

Use each mechanism for the job it actually does:

Mechanism Concrete job Cost or limit
Cached seat map Let many people inspect likely availability Later hold requests may conflict
Short database transaction Claim all requested seats exclusively Contention, waits and rejected claims
Waiting room Bound attempts reaching tested write capacity Fairness, queue age and token expiry
Authorization before capture Check inventory between approval and collection More states and provider-specific support
Payment-pending reservation Keep possibly paid seats exclusive during uncertainty Inventory stays unavailable until reconciliation

Limit simultaneous holds and protect catalog/payment administration to reduce hoarding or misuse. Validate price on the server and preserve authentication through every route; a queue token or model recommendation cannot bypass these checks.

Measure queued attempt age, hold conflicts, expiry rate, transaction latency, unresolved payment age, duplicate callbacks, pending refunds and any inventory-rule violation. Compare provider payment records with booking ownership, not just the count of callbacks received. Close by tracing who claims A7/A8, which result resolves a retry and which state prevents resale during an uncertain capture.

OPTIONAL DESIGN EXTENSIONWhere AI Fits in This Design

A model can optionally help people find an event before they try to claim seats. It cannot decide whether a seat is available or a payment succeeded.

  1. Input and output. For “family music this weekend,” use public event descriptions to suggest matching event IDs or city/date/category filters. Meaning-based retrieval can find related wording as well as exact terms. Validate filters and read current catalog facts before showing them; Elastic’s hybrid-search example illustrates combining these retrieval methods.
  2. Integration. Keep discovery separate from hold and checkout code. The inventory handler still checks current seats, server prices, authentication and saved request results. Generated text cannot reserve A7, extend H1 or confirm B1.
  3. Fallback and budget. Assume 100 ms for optional discovery ranking, measured separately from hold latency. On timeout or poor confidence, use ordinary performer, city and date search.
  4. Evaluation and privacy. Test relevant-event retrieval, false claims, date/language interpretation and accessibility or family-category errors. Measure slow calls and cost/search. Use public catalog text and minimized queries, not payment details or unnecessary private booking history. Organizer edits and deletions must reach any derived semantic index.

The deterministic inventory and payment flow remains the same without AI.

DRAW. CALCULATE. EXPLAIN.

Walk a hold through three failure races

75 minutes · guided practice

Design the seat and booking state machines for a two-minute hold. Trace two customers racing for A7, a buyer retrying after a lost hold response, and a payment succeeding after the hold expires. Add a two-seat request where one seat is already held. For each case, name the saved seat and booking records. Show when to roll back the seat group, when a retry returns the same hold, and when money without seats requires a saved refund task.

Your self-check
Compare the worked solution
  1. Claim one group. The hold handler locks A7/A8 in a consistent order, checks current rows with fresh server time, and commits both claims plus H1 and its key-to-result record. If either seat cannot be claimed, it rolls back the whole set.
  2. Resolve competition and reply loss. Only the first valid A7 claim commits; Leo’s competing request receives conflict. Mina’s matching retry returns H1 with its original expiry, not a fresh two-minute hold.
  3. Separate payment from locks. Save B1 and a stable payment operation. Authorize outside the seat transaction. Then recheck H1 and convert its seats to B1’s payment-pending reservation before requesting capture. H1 expiry before conversion may make the seats available; after conversion, an unknown capture keeps B1’s reservation protected.
  4. Resolve capture. Retry or query the same provider operation. Confirm after established success; release through a saved transition after definitive failure. Do not infer failure from a timeout or issue another charge ID.
  5. Handle the other payment branch. If a method charges before valid reservation and success arrives after H1 expired and another buyer claimed the seats, save refund-required and complete compensation. Do not displace that valid buyer.
  6. Resume saved work. The outbox retains commands not yet sent. The inbox recognizes repeated provider events; state checks reject backward updates. Reconciliation uses the provider reference when callbacks are missing. Explain these records rather than claiming that a queue alone completes the purchase.

The checkboxes record your own assessment. The worked solution gives you a way to check the reasoning.

RETRIEVE BEFORE YOU REVEAL

Keep the important ideas.

Say the answer aloud, then turn the card over.

CARD 01What actually prevents double booking?

The hold handler checks current event-seat rows and expiry, claims the complete requested group and saves the hold plus retry result in one transaction. A map or waiting-room token grants no seat. Matching retries return the original hold deadline.

CARD 02Why distinguish shopping hold from payment-pending reservation?

A shopping hold expires after two minutes. After authorization and a fresh hold check, convert its seats to the booking’s payment-pending reservation. If capture is unknown, keep them reserved and query the same provider operation. Established success confirms the booking; definitive failure permits a saved cancellation and release.

CARD 03How are payment and retry failures recovered?

Reuse the payment operation ID after a lost reply. Save outgoing work and received event IDs, check current booking state and ask the provider about uncertain outcomes. Money collected without a valid inventory claim needs a recorded refund, not a second owner.

Practise this topic in the revision deck ↗

CHECK THE REASONING

Can you explain the choice?

Answer every question, then finish for an immediate score and an explanation for each answer. Aim for 80%, then revisit any gaps.

1 / 10Mina requests A7/A8 while A8 is already held. What does all-or-nothing mean?
2 / 10Both buyers saw A7 as available. Which result is legitimate?
3 / 10Why lock seats in a consistent order?
4 / 10A successful hold response is lost. The buyer repeats the same key and seat set. What should happen?
5 / 10The expiry cleanup worker is ten seconds late. How is a new claim decided?
6 / 10Capture times out after seats became payment-pending. What is the safe stated behavior?
7 / 10A late immediate capture succeeds after the hold expired and another buyer owns the seats. What next?
8 / 10What does a waiting room primarily control?
9 / 10A semantic event assistant recommends E1 and claims A7 is available. What establishes a purchase claim?
10 / 10A stale pending callback arrives after capture was confirmed. What should processing do?
0 of 10 answered

CLOSE THE LOOP

Explain it without the page.

Summarise the mechanism, one trade-off, and one failure mode. Then mark this chapter complete.

Go to the source