IRCTC Tatkal booking
Not climbed yetModel answer
Endpoints:
GET /trains?from=NDLS&to=HWH&date=...to search,GET /trains/{number}/availability?date=...&class=3A"a=TATKALfor seats left, andPOST /bookingswith the passengers, which holds seats and returns a booking to pay for. The payment confirms it, andGET /bookings/{pnr}shows its status.trainsandstops: the route, with the time at each station.inventory(train_no, date, class, quota, available): one row per run, class and quota. This is the counter that booking changes.bookings(pnr, user_id, train_no, date, class, status, amount, hold_expires_at)andpassengers(pnr, name, age, berth).
A booking moves through states:
HELD(seats taken, waiting for payment), thenCONFIRMED, orEXPIREDif the hold runs out, orCANCELLED. When confirmed berths run out, Indian Railways still takes bookings into RAC and a numbered waiting list, which are just more states with a position.What makes trains harder than cinema seats: a berth sold from Delhi to Kanpur is free again from Kanpur to Howrah. A first version counts seats for the whole run; a better one records which stretch of the route each berth is sold for.
Model answer
The bug to avoid is read-then-write: both requests read
available = 1, both decide there is room, both write0, and both get a ticket for the same berth.Make the check and the change one statement, and look at how many rows it changed:
UPDATE inventory SET available = available - 1 WHERE train_no = $1 AND date = $2 AND class = $3 AND quota = $4 AND available > 0; -- 1 row changed: you got it. 0 rows: sold out, offer the waiting list.The update takes a row lock, so the second request waits, then finds
available = 0and changes nothing.SELECT ... FOR UPDATEfollowed by the update, in one transaction, does the same by hand. The optimistic alternative is aversioncolumn (update onlyWHERE version = $seen, retry if no row matched), which suits rare conflicts; on the last berth of a busy train it means a storm of retries.To hand out actual berths,
SELECT ... FOR UPDATE SKIP LOCKED LIMIT 1over the free berths lets many bookers each lock a different one instead of queueing for the same row.Model answer
Two million in five minutes is about 6,700 new people a second, each refreshing availability again and again. Assume the booking database can safely take 2,000 bookings a second. The design is about keeping the crowd away from it.
- A virtual waiting room: everyone joins a queue and is let in at the rate booking can handle. Entry gives a short-lived signed token that the booking API checks, so calling the API directly does not skip the queue.
- Cache availability for a few seconds and show it as approximate; the real check is the booking statement. Static pages and assets come from a CDN.
- Pre-scale before 10:00. Autoscaling takes minutes to react, and the rush is over in five.
- Bots: a captcha before the queue, limits per account and per IP address, and one booking in progress per account.
The hot row: every booking on a popular train updates the same
inventoryrow, and its lock lets one through at a time. Keep that counter in Redis and take a seat with an atomic decrement (a Lua script that refuses to go below zero), then write the booking to SQL. Redis turns the crowd away fast and SQL stays the record. A job reconciles the two, and Redis is reloaded from SQL if it restarts.Model answer
Give holds an expiry (say 10 minutes) and the booking a state machine:
HELDtoPAYMENT_PENDINGtoCONFIRMED, orHELDtoEXPIRED, orPAYMENT_PENDINGtoFAILED. Each move is a conditional update, such asUPDATE bookings SET status = 'CONFIRMED' WHERE pnr = $1 AND status = 'PAYMENT_PENDING', so a move the machine does not allow changes nothing.A timeout is unknown, not failed. Mark the booking
PAYMENT_PENDING, keep the seat held a little longer, and ask the gateway for the payment status with backoff. The gateway's callback may still arrive, late or twice, so handle it idempotently, keyed by the payment id.If the money arrives after the hold expired and the berth went to someone else, never take it back from them. Confirm another free berth if there is one; otherwise refund automatically and tell the user. Berths are only ever assigned through that one conditional update, so two bookings cannot both hold one.
Then reconcile every day: match the gateway's settlement report against bookings and refund any payment without a confirmed ticket. The trade-off to name: holding seats longer for slow payments is kinder to this user and worse for everyone still in the queue.
