Design problems
Booking & traffic spikesHardSDE-3 40 min

Design Tatkal train booking (IRCTC)

Asked at
AmazonMakeMyTripPaytmPhonePe

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

admission control and durable request queueserver-side window open timeper-user and per-train idempotencysingle-writer allocator per train-date-classKafka partition per train for orderingsnapshot plus log replay for failoversegment bitset per berthRAC and waitlist upgradesshort payment window with release and refundbot defence and agent restrictions

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

Users at window open~2-3M logged inTatkal on peak travel days (summer, Diwali, Chhath) draws millions; many open multiple tabs.
Booking submissions~50k-100k/s for ~30 s~2M serious attempts arriving within ~30-40 s of 10:00, plus retries; total requests are several times higher with refreshes.
Inventory per train-class~20-300 Tatkal berthsTatkal quota is a small share of each class; e.g. one AC 3-tier coach has 72 berths, a train may have a few Tatkal coaches' worth.
Train-class combinations per day~10-20k~3,000-4,000 long-distance trains running per day × ~4 classes, Tatkal opened for the next day.
Successful bookings~200-400k Tatkal tickets/dayBounded by quota, not demand; most attempts end in waitlist or failure. A whole-system record is ~25-30k tickets per minute.
Hot spotTop 100 trains get ~50% of requestsPopular routes (Delhi-Patna, Mumbai-Gorakhpur, Delhi-Mumbai) dominate; per-train request rate can reach thousands/s.
Payment window load~3-5k payments/sAllocations happen in the first minute, so payment volume concentrates right after.

3 API

EndpointWhat it does
POST/v1/auth/loginLogin with captcha and OTP; Aadhaar-verified accounts required for Tatkal; returns session.
GET/v1/trains/{no}/availability?date=&class=&quota=TQ&from=&to=Cached availability snapshot; may be seconds stale.
POST/v1/tatkal/requestsSubmit {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}/payStart payment for an allocated booking; payment window ~5 minutes.
POST/v1/webhooks/paymentsBank/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_key

The 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 counters

Authoritative 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_deadlineamount

Written by the allocator output stream; source for tickets and chart preparation.

users and saved passengersPostgres + Redis session store
user_idkyc_statusphonesaved_passengers[]; session:{id}

Read at login time, not at 10:00; sessions in Redis.

availability cacheRedis
avail:{train}:{date}:{class}:{quota} → {availableracwlts}

Published by allocators every second; search reads only this.

paymentsPostgres
payment_idpnramountstatusgateway_refidempotency_key

Reconciled with banks and UPI; refunds generated automatically for released PNRs.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALsubmit at 10:00poll statusappend requestin orderresult, availabilityPNR, snapshotsync allocationspaid / failedtimeout releaseUsers (app / web)CDN + bot defenceAPI gateway + ratelimitPayment svcRequest intake svcStatus / search svcKafka (pertrain-class)Seat allocatorsPayment-timeoutreleaserPostgres (PNR,snapshots)Redis (status, avail)Banks / UPI / gatewaysReservation core (PRS)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Before the window (09:30-09:59)

  1. 1Users log in with captcha and OTP; session tokens live in Redis; login is rate limited per account and IP.
  2. 2Users search and pre-fill the booking form from saved passengers; search is served from the availability cache and CDN.
  3. 3Bot defence on the CDN scores clients (behaviour, device fingerprint, IP reputation) and requires challenges from suspicious ones.
  4. 4The intake service is pre-scaled; Kafka partitions and allocators for today's Tatkal trains are warmed with inventory snapshots.

Submit at 10:00

  1. 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).
  2. 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.
  3. 3It returns request_id immediately (HTTP 202) with a rough position; the client polls status every few seconds from Redis.
  4. 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

  1. 1The allocator for train 12309/2A/TQ reads requests in partition order and, for each, searches for berths free over [from, to) matching preferences.
  2. 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.
  3. 3When confirmed berths run out it assigns RAC, then waitlist numbers, until the waitlist limit; after that requests fail fast with 'not available'.
  4. 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

Q1How would you handle a user who submits for two different trains at 10:00?
Q2How do you prove to an auditor that requests were served in order?
Q3How do you upgrade waitlisted passengers when someone cancels after charting?
Q4What would you change to support 10x more trains with Tatkal?
Q5How do you keep normal (non-Tatkal) bookings fast during the spike?
Q6How would you detect a booking agent using many personal accounts?

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.

Start free