~learn.

IRCTC Tatkal Ticket Booking System, the whole design.

Opens a ~500-seat Tatkal quota to ~2 million people at 10:00:00 and sells each seat exactly once, by saying "no" to 99.975 % of them at the edge in milliseconds so only traffic the size of the inventory reaches the database.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-11 · IRCTC Tatkal Ticket Booking System · design sheethimanshukukreja.in/learn/sheets/bonus-11-irctc-booking-system

At a glance#

Number Value What it drives
Attempts on one hot train ~2 million in 60 s (assumed) 4,000 requests for every seat
Tatkal quota ~500 seats (assumed) 99.975 % of attempts must fail, fast and fairly
Opening burst ~280,000 req / s assumes ~70 % land in the first 5 s
Whole platform, record minute 37,410 tickets (16 Aug 2025) ~620 / s; one unthrottled train ≈ 450× that
Site capacity (since Jul 2026) 40 lakh+ enquiries vs 1.5 lakh+ bookings / min reads dwarf writes: separate planes
Latency budgets < 200 ms admit · < 1 s hold · < 15 s to PNR reject at the edge, never in the database
Storage ~700 B per PNR · ~350 KB for 500 winners losers store 0 bytes; disk is never the bottleneck
Seat hold 5 min TTL abandoned carts give the seat back

Architecture#

DiagramTatkal on one page: read plane, waiting room, hot pool, saga
%% title: Tatkal on one page: read plane, waiting room, hot pool, saga
flowchart TD
  U@{ icon: "sd:users", label: "~2M people at 10:00:00<br/>web · app · counters · agents" } -->|"search"| RP@{ icon: "sd:cdn", label: "Read plane · CDN + cache<br/>search · fares · availability<br/>snapshots 1–2 s stale" }
  U ==>|"book"| WR@{ icon: "sd:queue", label: "Virtual waiting room<br/>token · shuffled 250 ms buckets<br/>adaptive admit rate" }
  WR ==> RISK["Risk gate<br/>Aadhaar OTP · agent window · velocity"]:::service
  RISK ==> GATE@{ icon: "tech:redis", label: "Redis admission counter<br/>one per pool · atomic Lua" }
  GATE -->|"0 left"| NO["Sold out · < 200 ms<br/>no durable write"]:::bad
  GATE ==>|"~500 tickets to try"| AL["Seat allocator<br/>single writer per pool · assigns berths"]:::hot
  AL ==> SAGA["Booking saga<br/>hold 5 min → pay → PNR<br/>or release + refund"]:::core
  SAGA --> PG@{ icon: "sd:card", label: "Payment gateways<br/>UPI · cards · netbanking" }
  SAGA --> DB@{ icon: "sd:database", label: "PNR / ticketing DB<br/>active-passive" }
  SAGA --> NT@{ icon: "sd:sms", label: "SMS / push" }
  PG -.-> REC["Reconciliation worker<br/>deemed pending → PNR or refund"]:::service
  DB --> WL["Waitlist promotion · chart prep<br/>single writer, strict order"]:::service
  class U client
  class RP,GATE cache
  class WR queue
  class PG,NT external
  class DB store
DiagramHot pool: "can I even try?" before "give me the seat"
%% title: Hot pool: "can I even try?" before "give me the seat"
flowchart TD
  REQ["Admitted request · attempt_id"]:::core --> LUA@{ icon: "tech:redis", label: "Lua script on pool counter<br/>pool:12301:date:3A:TQ · starts at 500" }
  LUA --> SEEN{"attempt_id already admitted?"}
  SEEN -->|"yes · retry"| SAME["Same ticket to try<br/>no second seat burned"]:::ok
  SEEN -->|"no"| C{"counter > 0?"}
  C -->|"no"| SOLD["SOLD_OUT · in single-digit ms"]:::bad
  C ==>|"yes · DECR + remember id"| AL["Single-writer allocator<br/>ordinary transactional UPDATE"]:::hot
  AL --> DB@{ icon: "sd:database", label: "Real database<br/>assigns berth numbers" }
  class LUA cache
  class DB store

Key flows#

DiagramPriya wins a seat: queue, gate, hold, pay, PNR
%% title: Priya wins a seat: queue, gate, hold, pay, PNR
sequenceDiagram
  autonumber
  participant A as Priya's app
  participant W as Waiting room
  participant G as Pool counter
  participant S as Booking saga
  participant P as Payment gateway
  Note over A: 09:59:50 · search prefetched, session open
  A->>W: 10:00:00 · request queue token
  W-->>A: Token · position 84,213 · ~2 s
  W->>G: Admit (rate set by core health)
  G-->>S: Ticket to try · counter 500 → 499
  S-->>A: Hold granted · TTL 5 min
  A->>S: Passengers + Aadhaar OTP · idempotency key
  S->>P: Charge (same key)
  P-->>S: Success
  S-->>A: PNR confirmed · 3A, berth 24
  Note over A,P: ~8–12 s for a winner · 1,999,500 others got "no" in < 200 ms
DiagramPayment times out: park it, reconcile, never guess
%% title: Payment times out: park it, reconcile, never guess
sequenceDiagram
  autonumber
  participant S as Booking saga
  participant P as Payment gateway
  participant R as Reconciliation worker
  participant G as Seat pool
  participant A as Priya
  S->>P: Charge (attempt ID, idempotency key)
  P--xS: timeout
  Note over S: mark DEEMED_PENDING · TTL job leaves this hold alone
  R->>P: Status check (same attempt ID)
  alt paid
    P-->>R: SUCCESS
    R->>A: PNR confirmed
  else failed
    P-->>R: FAILED
    R->>G: Release seat to the pool
    R->>A: Refund + clear message
  else still unknown
    P-->>R: PENDING
    Note over R: check again in 10 min · resolve within 30 min
  end
DiagramAdaptive waiting room: admit only what the core can absorb
%% title: Adaptive waiting room: admit only what the core can absorb
sequenceDiagram
  autonumber
  participant C as Client
  participant W as Waiting room
  participant B as Booking core
  C->>W: Connect before 10:00:00
  Note over W: one slot per session · shuffle inside 250 ms buckets
  loop every 1 s
    B-)W: DB latency · CPU · payment success
    Note over W: healthy → 1,000/s · DB > 50 ms → halve · floor 100/s
  end
  W-->>C: Admitted in small batches
  W-->>C: or "sold out" / "position 340,201", in ms

Data model#

DiagramCore entities
%% title: Core entities
classDiagram
  class Pool {
    +poolKey : train · date · class · quota
    +seatsTotal : int · about 500
    +counter : int · Redis
    +opensAt : 10 AM AC · 11 AM non-AC
  }
  class BookingAttempt {
    +attemptId : UUID
    +idempotencyKey : string · one per attempt
    +poolKey : string
    +state : BookingState
    +seatRef : berth
    +paymentRef : string
    +holdExpiresAt : timestamp · plus 5 min
  }
  class PNR {
    +pnr : string · the PNR number
    +passengers : 1 to 6
    +berths : coach · berth
    +status : CNF / RAC / WL
    +paymentRef : string
  }
  class QueueTicket {
    +sessionId : string · one active slot
    +bucket : 250 ms window
    +shuffleKey : hash of session + bucket
  }
  class WaitlistEntry {
    +pnr : string
    +position : int · strict order
  }
  Pool "1" --> "many" BookingAttempt : admits
  BookingAttempt "1" --> "0..1" PNR : confirms as
  QueueTicket "1" --> "0..1" BookingAttempt : becomes
  Pool "1" --> "many" WaitlistEntry : orders

Lifecycle#

DiagramBooking attempt: a saga with a TTL
%% title: Booking attempt: a saga with a TTL
stateDiagram-v2
  [*] --> ADMITTED: through waiting room + gate
  ADMITTED --> SEAT_HELD: selects seat
  SEAT_HELD --> HOLD_EXPIRED: 5 min TTL
  SEAT_HELD --> HOLD_RELEASED: abandons
  SEAT_HELD --> PAYMENT_INITIATED: details + OTP
  PAYMENT_INITIATED --> PNR_CONFIRMED: paid
  PAYMENT_INITIATED --> COMPENSATED: failed · release + refund
  PAYMENT_INITIATED --> DEEMED_PENDING: timeout
  DEEMED_PENDING --> PNR_CONFIRMED: gateway says paid
  DEEMED_PENDING --> COMPENSATED: gateway says failed
  PNR_CONFIRMED --> [*]
  COMPENSATED --> [*]
  HOLD_EXPIRED --> [*]
  HOLD_RELEASED --> [*]
  class PNR_CONFIRMED ok
  class COMPENSATED,HOLD_EXPIRED,HOLD_RELEASED bad
  class DEEMED_PENDING queue

Decisions#

Decision Chose Over Because
Search vs booking Physically separate planes Shared tiers, separate endpoints opposite failure modes; search may be 1–2 s stale
Hot pool Dumb Redis counter, then a single writer DB row lock, or 10 shards of 50 280K/s on one row is lock churn; shards = 10 hot rows
Queue order Shuffle inside 250 ms buckets Pure first-come first byte wins rewards scripts, not humans
Admit rate Adaptive, from core health every 1 s Fixed rate fixed either wastes capacity or overloads
Hold then pay Saga, 5 min TTL, compensation Hold until the user returns abandoned tabs never call back
Payment timeout DEEMED_PENDING + reconcile Guess success or failure no phantom PNR, no lost rupee
Isolation Bulkhead per (train, date) pool One shared inventory cluster one hot train never slows the rest

When it breaks#

Failure What users see Contain it with
Search spike shares booking infra (Dec 2024) "e-ticketing not available for the next hour" separate tiers; rate-limit enquiries hard
DB latency crosses 50 ms longer wait in the queue admit rate halves; waiting is cheap at the edge
Payment gateway times out "pending", then PNR or refund DEEMED_PENDING, status checks, resolve in 30 min
Bank confirms after the hold expired refund with priority + clear message expiry marks for reconciliation, never a silent loss
One hot train's 280K/s burst other trains unaffected per-pool counters, allocators, connection pools
Bots and bulk agents blocked or asked for extra checks Aadhaar OTP, no agents first 30 min, velocity + ML

Say it in the interview#