Picking seats is like putting a "reserved" card on a cinema seat for 10 minutes while you go and pay. The rule that makes it safe: you can only put your card on a seat that has no card, and only one person can place a card at a time.
Design a ticket booking system like BookMyShow or Ticketmaster: users browse shows, pick specific seats on a seat map, hold them for a few minutes while paying, and get a confirmed booking. The defining constraint is that a seat must never be sold twice, even when thousands of people try to book the same popular show in the same second.
GET /v1/shows?city=blr&movie=123&date=2026-10-02 -> shows (cached)
GET /v1/shows/{show_id}/seats -> seat map with status
POST /v1/shows/{show_id}/holds { seat_ids: ["F7","F8"] }
-> 201 { hold_id, booking_id, expires_at } | 409 { unavailable: ["F8"] }
POST /v1/bookings/{booking_id}/pay { payment_method } Idempotency-Key: ...
-> 202 (redirect to payment gateway)
payment webhook -> confirms booking
GET /v1/bookings/{booking_id} -> { status, seats, ticket_qr }
client -> CDN / API gateway
|-- browse: Catalog service -> cache (Redis) -> catalog DB (read replicas)
|-- seat map: Inventory service -> seat status (DB + short-lived cache)
|-- hold/book: Booking service --(transaction)--> Inventory DB (show_seats)
| (sharded by show_id)
+--> Payment service --> payment gateway
| webhook --> confirm booking
+--> expiry worker: releases holds past expires_at
+--> Notification (ticket email/SMS)
Hot sale: Virtual waiting room in front of the booking flow (admits users at a
controlled rate; everyone else waits in a queue page)
show_seats(show_id, seat_id, status, hold_expires_at, booking_id, version,
PRIMARY KEY (show_id, seat_id))
-- Hold: one conditional UPDATE for all requested seats, inside a transaction
BEGIN;
UPDATE show_seats
SET status = 'HELD', booking_id = :b, hold_expires_at = now() + interval '10 min'
WHERE show_id = :show AND seat_id IN ('F7','F8')
AND (status = 'AVAILABLE' OR (status = 'HELD' AND hold_expires_at < now()));
-- rows_affected must equal 2; otherwise ROLLBACK and return 409
COMMIT;
The WHERE status = 'AVAILABLE' condition is the whole trick: the database's row locks make two concurrent holds on F8 serialize, and the second one updates zero rows. No application-level lock is needed. An alternative is a Redis SET seat:{show}:{seat} NX PX 600000 per seat for the hold, with the database as the final record; faster under extreme contention but two systems to keep consistent.
payment success webhook (idempotent by provider reference):
UPDATE show_seats SET status='BOOKED'
WHERE booking_id=:b AND status='HELD' AND hold_expires_at > now();
if all seats updated -> booking CONFIRMED -> send ticket
else (hold expired while paying) -> refund automatically, apologise
expiry worker every few seconds:
UPDATE show_seats SET status='AVAILABLE', booking_id=NULL
WHERE status='HELD' AND hold_expires_at < now();
show_id: a hot show lives on one shard, but it's only a few hundred to a few thousand rows, well within one database's capacity when admission is controlled.status='AVAILABLE'. The database locks the row for the first; the second waits, then re-evaluates the condition, finds HELD, updates 0 rows and returns 409. The UI asks the second user to pick another seat.