Skip to content

Scenario: ticketing on-sale with seat holds

Hold with a timed row and a guarded update, guard the confirm too, and let the waiting room absorb the crowd.

Predict first

Before you read: predict

ExamHall's ECON-201 re-sit booking opens 300 seats at 10:00 for 2,400 eligible candidates. A candidate holds a seat while she pays; nobody else may take a held seat, and a hold ends 10 minutes after it was taken. Ana takes seat 17 at 10:00, then closes her tab. Ben tries seat 17 at 10:03, and Cara at 10:12.

Which way of taking the hold tells Ben at once that the seat is taken, and gives it to Cara?

Pick one

The 30-second version

An on-sale is a synchronised spike on a tiny inventory. Two deep dives decide it. Never double-sell: a hold is a guarded update on a row with an expiry, a lapsed hold counts as free, and a confirm is guarded too. Survive the rush: a waiting room admits what checkout can hold; the seat map is cached.

Hold with a timed row and a guarded update, guard the confirm too, and let the waiting room absorb the crowd.

How it works

Step 1 of 13

An on-sale is a synchronised spike on a tiny inventory. Most requests come from people looking at the seat map; few are holds.

Analogy: A sleeper train's berth reservations

Every berth on the night train is reserved days ahead, and the attendant's berth list shows whose each berth is.

On the sleeper train In the on-sale
a reservation keeps a berth until 15 minutes before departure a hold row with held_until (steps 4–5)
an unclaimed berth goes back on sale at that time; nobody has to ring the passenger a lapsed hold counts as free (step 5)
the platform gate lets passengers through only as fast as the attendants can settle them the waiting room at checkout's cap (steps 11–12)
the departures screen shows berths left, updated every few minutes the cached seat map (step 13)

Breaks down when: an attendant who sees a passenger running up the platform can wait for her. A server never learns that a holder closed her tab, so only the clock ends the hold (step 5).

Contrasting case: an attendant who stands in a compartment doorway, keeping it, while one passenger phones her bank. Nobody else on that coach can be shown to any berth until the call ends (step 3).

In real terms: a guarded update on a timed row is the only lock; everything else is crowd control and a cache.

Examples, run for real

4.1 ExamHall EXH-3940: the row lock that held the pool

ExamHall's re-sit booking locked each seat row with SELECT … FOR UPDATE and kept the transaction open through the 3-minute payment. The pool had 20 connections, and the waiting room let 300 candidates in during the first minute.

4.1 before: FOR UPDATE kept through the 3-minute payment · 20 connections · 300 candidates in the first minute · waiting for a connection at 10:01: 280
4.1 MR !4240: guarded update on held_until, connection given back after each statement · waiting for a connection at 10:01: 0

MR !4240 takes the hold as a guarded update on held_until and gives the connection back after each statement, so the payment runs with no connection held (steps 3–5).

4.2 Most of the load is looking (payments ledger)

A payments app sells a limited issue of savings bonds. Its 20,000 buyers each reload the "bonds left" page every 15 seconds for the first 10 minutes.

4.2 20,000 buyers reload the page every 15 s for 10 minutes: 800,000 page loads · with a 5-second cache: 120 database reads · without it: 800,000

The cache answers the page; the database sees one read per refresh and one short write per hold (steps 1–2, 13).

4.3 One bond's hold, charge and confirm (payments ledger)

Each buyer's flow on PostgreSQL: hold, charge, then confirm or release. A confirm with no hold behind it is refused too.

4.3 PostgreSQL 18.3 · Ola: hold 1 row, card declined, release 1 row, bond 1 free, charges kept 0 · Pia: hold 1 row, charged, confirm 1 row, bond 2 sold to Pia · Ray: confirm with no hold 0 rows, bond 3 free

A failed charge gives the bond back at once, and the ledger shows no charge left on it (steps 4, 8–9).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 6

A streaming service sells 500 seats for a premiere's watch party. A fan's hold lasts 10 minutes (held_until), and the hold update also takes a seat whose hold has lapsed. After a payment succeeds, the order service runs UPDATE seats SET status = 'sold', sold_to = $me WHERE id = $seat. Dana holds seat 9 at 20:00, and her bank answers at 20:24. Eli holds seat 9 at 20:21 and pays at 20:27. Both are issued a ticket for seat 9.

Which change leaves exactly one ticket for seat 9, issued to Eli?

Pick one

Question 2 of 6

A payments app sells 5,000 savings bonds from 10:00, and 20,000 people arrive at once. Its checkout handles 400 buyers at a time, so its Cloudflare waiting room has total active users 400, new users per minute 250 and a session duration of 10 minutes. A buyer loads a page on entering and another 2 minutes later, pays 1 minute after that, and leaves the site. The team wants a paid buyer's place given to the next buyer that minute, never more than 400 buyers past the room, and no buyer sent back to the queue mid-checkout.

Which change does it?

Pick one

Question 3 of 6

In a design round you said a hold is a guarded update that also takes a seat whose held_until has passed, the seat map is served from a cache refreshed every 5 seconds by SELECT id FROM seats WHERE status = 'free', and a cleanup job frees lapsed holds every 10 minutes, at :00, :10, :20 and so on. The interviewer says: "Kim held seat 17 at 19:01 for 10 minutes and closed her tab. Lea loads the map at 19:12." She wants a lapsed seat shown free within one refresh, with the cache kept.

Which answer survives?

Pick one

Question 4 of 6

A music festival sells 200 camping pitches. A hold is a row in holds (pitch_id PRIMARY KEY, holder, expires_at): the hold inserts the pitch with expires_at = $now + 10 min, and an insert that the key refuses, or that changes no row, tells the buyer "taken". A cleanup job deletes rows past expires_at at :00, :15, :30 and :45. Sam holds pitch 30 at 09:01. Omar holds pitch 12 at 09:02 and closes his tab. Rui tries pitch 30 at 09:05, and Pia tries pitch 12 at 09:13. The team wants Pia to get pitch 12 at 09:13 and Rui to be told "taken".

Which change does it?

Pick one

Question 5 of 6

A payments app sells 400 savings bonds at 18:00, and far more buyers are queuing than there are bonds: a queuing buyer tries each held bond every 30 seconds, so a lapsed hold is taken within 30 seconds. A hold lasts a fixed time from the moment it is taken and is never extended. A confirm is the guarded holder = $me update, and a charge whose confirm changes 0 rows is refunded. In the rehearsal, buyers spent about 2 minutes on the bond list before taking a hold, then 1 to 6 minutes entering their card details (most took 2), and the card provider answered within 2 minutes after that.

Which is the shortest hold that refunds no paying buyer?

Pick one

Question 6 of 6

A chat app sells 300 seats for a live Q&A. Each hold is one short guarded update, but its Pay handler opens a transaction, locks the seat row with SELECT … FOR UPDATE, waits for the card provider, then confirms and commits. The app's pool has 10 connections, shared with the seat map's cache refill, which is one short query on no seat row. The provider answers in 2 minutes. At 20:00, 15 buyers press Pay, each on her own seat. At 20:01 the refill asks for a connection.

When does it get one?

Pick one

Go deeper

Pro tips
  1. Say the read-to-hold ratio out loud before drawing anything, with the two numbers it comes from. When: the first minutes of an on-sale round, before you pick a database or a cache (steps 1–2). Verify:
    tip 2,400 candidates × a seat-map reload every 10 s × 10 minutes = 144,000 reads · 300 seats = at least 300 holds · about 480 reads per hold
    Status: RAN.
Source

quote-check.mjs finds every passage below verbatim in its saved text. The row names exactly these two sources (the Cloudflare docs as four pages of one site). Valid as of 2026-10-13.

  • Hello Interview, Design a Ticket Booking Site Like Ticketmaster: https://www.hellointerview.com/learn/system-design/problem-breakdowns/ticketmaster (hellointerview-ticketmaster.txt). The row's first source. Quoted: "database locks are meant to be used for short periods of time (a single, near-instant, transaction)"; "We check to see if the current ticket is either AVAILABLE or (RESERVED but expired)"; "users will be able to claim tickets from expired reservations"; "There's an inherent delay between the ticket expiration and the cron job execution"; "If User A's lock expires at minute 10 but their payment completes at minute 11, User B could have grabbed the lock in between."; "we issue an automatic refund via Stripe for the failed booking"; "Set the TTL generously to minimize this, and, even better, consider extending the lock when payment is initiated."; "thousands of users refresh the same event page waiting for availability". Supports steps 1, 3–6, 8–10 and the tip.
  • Cloudflare, Waiting Room docs: https://developers.cloudflare.com/waiting-room/ (cloudflare-waiting-room.txt), About https://developers.cloudflare.com/waiting-room/about/ (cloudflare-waiting-room-how.txt), Configuration settings https://developers.cloudflare.com/waiting-room/reference/configuration-settings/ (cloudflare-waiting-room-settings.txt), Queueing method https://developers.cloudflare.com/waiting-room/reference/queueing-methods/ (cloudflare-waiting-room-queueing.txt). The row's second source. Quoted: "Waiting Room protects your origin server by preventing surges in legitimate traffic that may overload your origin."; "total active users is a target threshold for how many simultaneous users you want to allow on the pages covered by your waiting room."; "new users per minute defines the target threshold for the maximum rate of user influx to your website per minute."; "A user is considered active for session duration minutes since the request is made to any page covered by a waiting room."; "If false, a user's session cookie is renewed on every request."; "Revoke a user's session when they perform a specific action by sending a command to the waiting room using an HTTP header on the response from your origin."; "Enable this setting in the dashboard"; "Cf-Waiting-Room-Command: revoke"; "greater than 200 and less than or equal to total active users"; "The queueing method determines the order that visitors exit an active waiting room and reach your application."

Checked against the tools and docs as of 2026-10-13.