Design Tatkal train booking (IRCTC)
The interviewer asks
“Design the Tatkal booking system for Indian Railways. Tatkal quota opens at 10:00 for AC classes and 11:00 for non-AC, one day before the journey, and a few thousand seats on popular trains sell out in minutes while millions of users try at the same second. How do you keep it fair, fast and correct?”
What a strong answer looks like
A strong answer starts from the extreme shape of the load: almost zero traffic at 09:59:55, then millions of logged-in users submitting at 10:00:00 for a small, fixed inventory. Scaling everything for that second is impossible and unfair anyway, so the design controls admission: users log in and pre-fill passengers before the window, a booking queue accepts requests at 10:00 with server-side timestamps and anti-bot checks, and an allocation tier processes them per (train, date, class, quota) in order. Inventory for one train-class is tiny (tens to a few hundred Tatkal berths), so a single-writer allocator per train-class in memory, backed by a durable log, beats a contended database row. Seat allocation is segment-based (a berth can be sold for different station ranges) and falls back to RAC and waitlist. Payment happens after a seat is provisionally allocated, with a short payment window and automatic release or refund. Deep dives: the admission queue and fairness, the per-train allocator, segment-based seat inventory, and the payment window.
Concepts to mention
1 Requirements
Functional
- Users log in, save passenger lists and search trains and availability before the Tatkal window opens.
- At window open, users submit a booking request for a train, date, class, boarding and destination stations and up to 4 passengers.
- The system allocates confirmed berths if available, otherwise RAC or waitlist, honouring berth preferences where possible.
- Users pay within a short window; the PNR is confirmed and a ticket is sent by SMS/email.
- Agents are blocked during the first 30 minutes of each Tatkal window; limits per user apply.
- Unpaid or failed bookings release berths back to the pool; failed payments are refunded automatically.
Non-functional
- Peak: millions of users online at 10:00, ~50k+ booking requests per second for the first ~30 seconds.
- Fairness: requests for the same train are served in arrival order at our edge; bots and scripts must not get an advantage.
- Correctness: no berth sold twice for overlapping segments; no money taken without a ticket or a refund.
- A user gets an outcome (booked, waitlisted, failed) within ~1-2 minutes, with clear queue status meanwhile.
- Normal-day booking and search must not be affected by the Tatkal spike.
- Data stays in India; audit trail for every allocation.
Out of scope (say it out loud)
- Train scheduling and chart preparation details.
- Fare calculation rules beyond a lookup.
- Cancellation refund rules for Tatkal (mention only).
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
POST/v1/auth/login | Login with captcha and OTP; Aadhaar-verified accounts required for Tatkal; returns session. |
GET/v1/trains/{no}/availability?date=&class="a=TQ&from=&to= | Cached availability snapshot; may be seconds stale. |
POST/v1/tatkal/requests | Submit {train, date, class, from, to, passengers[], preferences, Idempotency-Key}; returns request_id and queue position. |
GET/v1/tatkal/requests/{id} | Poll status: QUEUED → ALLOCATED (with payment deadline) / WAITLISTED / FAILED. |
POST/v1/bookings/{pnr}/pay | Start payment for an allocated booking; payment window ~5 minutes. |
POST/v1/webhooks/payments | Bank/UPI payment result; confirms PNR or releases berths and refunds. |
4 Data model
tatkal_request_logKafka (partitioned by train-date-class)request_iduser_idtraindateclassfromtopassengersedge_tsidempotency_keyThe ordered, durable queue of requests. Partition key = train-date-class so one allocator consumes one train in order.
seat_inventoryIn-memory allocator state, snapshotted to Postgres(train, date, class, coach, berth) → bitset of occupied segments; quota tags; RAC/WL countersAuthoritative state lives in the allocator's memory for the window, rebuilt from snapshot + log on failover.
bookings (PNR)Postgres sharded by PNR (or the existing PRS core)pnruser_idtraindateclassfromtopassengers[]berths[]status (ALLOCATED|CONFIRMED|RAC|WL|RELEASED|CANCELLED)pay_deadlineamountWritten by the allocator output stream; source for tickets and chart preparation.
users and saved passengersPostgres + Redis session storeuser_idkyc_statusphonesaved_passengers[]; session:{id}Read at login time, not at 10:00; sessions in Redis.
availability cacheRedisavail:{train}:{date}:{class}:{quota} → {availableracwlts}Published by allocators every second; search reads only this.
paymentsPostgrespayment_idpnramountstatusgateway_refidempotency_keyReconciled with banks and UPI; refunds generated automatically for released PNRs.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Before the window (09:30-09:59)
- 1Users log in with captcha and OTP; session tokens live in Redis; login is rate limited per account and IP.
- 2Users search and pre-fill the booking form from saved passengers; search is served from the availability cache and CDN.
- 3Bot defence on the CDN scores clients (behaviour, device fingerprint, IP reputation) and requires challenges from suspicious ones.
- 4The intake service is pre-scaled; Kafka partitions and allocators for today's Tatkal trains are warmed with inventory snapshots.
Submit at 10:00
- 1Requests sent before 10:00:00 server time are rejected at the gateway (the button is disabled client-side, but the server is the judge).
- 2The intake service validates session, Tatkal eligibility (verified account, per-user limits, agents blocked in the first 30 minutes) and form, stamps an edge timestamp, and appends the request to Kafka with key = train-date-class.
- 3It returns request_id immediately (HTTP 202) with a rough position; the client polls status every few seconds from Redis.
- 4Duplicate submits with the same Idempotency-Key, or a second request by the same user for the same train, return the existing request_id.
Allocate and pay
- 1The allocator for train 12309/2A/TQ reads requests in partition order and, for each, searches for berths free over [from, to) matching preferences.
- 2It marks berths in memory, appends an ALLOCATED result with a 5-minute payment deadline, writes the PNR row, and publishes the result to Redis for the user's poll.
- 3When confirmed berths run out it assigns RAC, then waitlist numbers, until the waitlist limit; after that requests fail fast with 'not available'.
- 4The user pays; on success the payment service emits PAID and the allocator marks the PNR confirmed; on failure or deadline expiry it releases the berths, which go to the next request in the queue or upgrade a waitlisted PNR.
7 Deep dives
Admission, fairness and bots
The worst design lets every request race to the database at 10:00:00; the winners are whoever has the fastest script, and the database melts. Instead, decouple accepting a request from processing it. The intake tier does only cheap, stateless checks and appends to a durable log, so it scales horizontally to ~100k requests/s. The order in the log per train is the fairness order: arrival at our edge, with no advantage from hammering because one user can have only one active request per train and date. Requests before the server's opening time are rejected, not queued, so clock tricks do not work. Bots are the real fairness threat: enforce login with OTP on verified accounts, captcha at login (not at 10:00, when it would slow real users), device and behaviour scoring at the CDN, per-account and per-IP limits, and block agents for the first 30 minutes. Show honest queue status. Load from hammering the status endpoint is absorbed by serving it from Redis with client-side backoff hints (Retry-After). Pre-window work (login, form fill, search) spreads load over 30 minutes instead of one second.
Single-writer allocator per train-class
A train-date-class has at most a few hundred Tatkal berths and receives thousands of requests per second at open. Putting that on a database row (UPDATE available = available - n WHERE available >= n) creates a hot row where every transaction waits on one lock. Since the data is small, use a single-writer allocator: one process owns a train-date-class partition, holds its berth bitsets in memory and processes requests sequentially from the Kafka partition. Each allocation takes microseconds, so even 10k requests/s for one train is easy, and there is no lock contention. Durability comes from the log: the allocator writes its decisions to an output topic and PNR rows, and snapshots state periodically. If it crashes, a standby takes over the partition (Kafka consumer group rebalancing, or a lease in etcd), loads the last snapshot and replays results since then; inputs already decided are skipped by request_id. Many partitions spread thousands of trains across ~50-100 allocator instances. This is the same pattern stock exchanges use for order books: serialise per instrument, parallelise across instruments.
Segment-based berth inventory
Train seats are not like cinema seats: a berth from New Delhi to Patna can also be sold from Patna to Howrah. Model each berth's journey as a bitset over the train's ordered stops (a train with 30 stops needs 29 segment bits). A request for stations i to j needs a berth whose bits i..j-1 are all free; allocation sets those bits. Searching is a scan over the class's berths with bitwise AND, which is fast for a few hundred berths. Quotas complicate it: Tatkal berths are tagged, and some quotas apply only to certain segments (for example pooled or remote-location quotas). Prefer allocations that keep long stretches free (best-fit on the segment) to reduce fragmentation, and honour preferences (lower berth for senior citizens) as a soft score. RAC (two passengers sharing a side-lower berth) and waitlist are counters per class with ordered lists; when a confirmed passenger cancels or fails to pay, the allocator upgrades RAC 1 to confirmed and WL 1 to RAC in order. Availability shown in search is a summary computed from these bitsets and published every second.
Payment window and money correctness
Allocation before payment is fairer (the user knows the outcome fast) but ties up berths for users who never pay. Bound it with a short payment window (~5 minutes) after allocation. The payment service creates a payment with idempotency key = PNR; UPI and netbanking results arrive by callback, sometimes late. Three cases: paid in time → confirm; failed or no response by the deadline → release berths through the allocator's log (as an event, so ordering stays consistent) and offer the user nothing more; paid after release → refund automatically, because the berths may already belong to someone else. A reconciliation job compares bank settlement files with PNRs every few minutes and fixes mismatches. The historical alternative used by IRCTC is pay-first then book, with automatic refunds when booking fails; it protects inventory from non-payers but leads to many refunds at Tatkal time. Either way, money movement must be idempotent and auditable, and refunds must be automatic and fast, because 'money deducted, ticket not booked' is the most common Tatkal complaint.
8 Trade-offs
Queue and async result vs synchronous booking
Synchronous booking holds a connection open through allocation and payment setup; at 50k requests/s that is millions of waiting connections and timeouts that users retry, making it worse. Accepting into a durable queue and returning 202 keeps the intake fast and makes retries harmless. The cost is a polling UX, which users accept if status is clear.
In-memory single writer vs database locking
Database row locks are simple and durable but serialise on the hot row with network round trips per transaction. An in-memory single writer gives microsecond allocations and natural ordering, at the cost of building snapshot, replay and failover. Given the extreme contention per train, the single writer is worth it.
Allocate-then-pay vs pay-then-allocate
Allocate-then-pay: fast, clear outcome, but berths can be held by non-payers for minutes. Pay-then-allocate: no unpaid holds, but many refunds and anxious users. A short payment window with immediate re-allocation to the next request balances both.
9 Bottlenecks
Login and session storms
If everyone logs in at 09:59, the auth service becomes the bottleneck. Encourage early login (sessions valid for the window), rate limit per account, cache user eligibility in the session, and scale auth separately from booking.
Status polling
Two million clients polling every second create 2M requests/s. Serve status from Redis via a lightweight endpoint, return a Retry-After hint that grows with queue size, and push results by SMS or app notification as well.
Payment gateway surge
Thousands of payments per second right after allocation can exceed bank and gateway limits. Spread across gateways and banks, and extend deadlines automatically for payments in progress at the gateway, so bank slowness does not release a paid berth.
10 Failure handling
Allocator instance crashes at 10:00:20
A standby takes the partition within seconds, loads the latest snapshot and replays decisions from the output topic. Requests not yet decided are processed in order. Users see a few seconds of delay, never a double allocation, because each request_id is decided once.
Kafka broker failure
Replication factor 3 with acks=all and min.insync.replicas=2 means accepted requests survive a broker loss. Leadership moves in seconds; intake retries appends with the same request_id.
Downstream reservation core is slow
If allocations must sync to an older central reservation system, publish them asynchronously and keep the allocator as the authority for the Tatkal window, with the central system updated through a reliable outbox. Pause new allocations only if sync lag exceeds a safety limit.
11 What impresses
- Explains why scaling for the 10:00 spike is the wrong goal, and controls admission instead.
- Uses a single-writer allocator per train-class and relates it to exchange order books.
- Models berths as segment bitsets over stops instead of simple seat flags.
- Covers payment after release with automatic refunds and bank reconciliation.
- Moves login, captcha and form filling before the window to flatten the spike.
- Treats fairness as a requirement and names concrete anti-bot measures.
12 Mistakes
- Every request runs a database transaction on the same hot row.
- Captcha and heavy checks at the exact moment of booking.
- Synchronous end-to-end booking with long timeouts, causing retry storms.
- Treating a berth as one unit for the whole journey.
- No plan for unpaid allocations or late payment callbacks.
- Trusting client clocks for the window open time.
13 Follow-ups
Lessons this design uses
Practice questions
- Train ticket Tatkal booking opens at 10:00 a.m. sharp. Average traffic is low, but at 10:00 it jumps by 50x. How does this change how you write requirements and estimates?
- At 10:00 AM sharp, Tatkal booking opens and traffic goes from 50,000 to several million requests per second, but only a limited number of seats exist. How do you keep the system up and fair?
- Every day at 10:00 AM, traffic to your train booking system jumps 50x within one minute as Tatkal opens, and seats sell out in minutes. Last week the site crashed. How do you prepare the system for this daily spike?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.