•CASE STUDY

Train Search and Seat Availability DB Design (IRCTC)

4 min read·716 words·Intermediate

Asked at

1 candidate report in Mar 2026

How to use this case study

SDE-2 / Mid

  • Design tables for trains
  • Routes (stops in order) and schedules
  • Write the query "trains from station A to station B on a date"

SDE-3 / Senior

  • Model seat availability per segment (a seat can be sold for different parts of the journey)
  • Classes and quotas
  • Index for fast search

Staff / Principal

  • Discuss the Tatkal-hour spike (caching search, protecting booking)
  • Waitlists
  • Consistency between search counts and actual booking

Problem RestatementProblem

Flipkart asked: design the database for train search like IRCTC (Indian Railways). A user searches trains from station A to station B on a date. We must return trains that stop at A and later at B (in that order), with departure and arrival times, and seat availability per class (Sleeper, 3AC, 2AC) and fare. A train passes through many stations, and a seat can be sold for different parts of the route (e.g., seat 12 from Delhi to Kanpur and again from Kanpur to Patna).

Tables

CREATE TABLE stations (station_code TEXT PRIMARY KEY, name TEXT, city TEXT);

CREATE TABLE trains (train_no TEXT PRIMARY KEY, name TEXT, runs_on_days BIT(7));   -- Mon..Sun

CREATE TABLE train_stops (                -- the route, in order
  train_no     TEXT REFERENCES trains,
  stop_seq     INT,                       -- 1, 2, 3, ...
  station_code TEXT REFERENCES stations,
  arrival_time TIME, departure_time TIME,
  day_offset   INT,                       -- 0 = same day as start, 1 = next day...
  distance_km  INT,
  PRIMARY KEY (train_no, stop_seq)
);
CREATE INDEX ON train_stops (station_code, train_no, stop_seq);

CREATE TABLE train_runs (                 -- one row per train per journey date
  run_id BIGINT PRIMARY KEY, train_no TEXT, journey_date DATE, UNIQUE (train_no, journey_date)
);

CREATE TABLE coaches (run_id BIGINT, coach_no TEXT, class TEXT, seat_count INT);

CREATE TABLE seat_bookings (             -- a seat is occupied between two stop numbers
  run_id BIGINT, coach_no TEXT, seat_no INT,
  from_seq INT, to_seq INT,              -- occupied from stop from_seq up to (not including) to_seq
  pnr TEXT
);
CREATE TABLE availability (              -- precomputed counts for fast search
  run_id BIGINT, class TEXT, from_seq INT, to_seq INT, available INT,
  PRIMARY KEY (run_id, class, from_seq, to_seq)
);

The Search Query

SELECT a.train_no, a.departure_time, b.arrival_time
FROM train_stops a
JOIN train_stops b ON b.train_no = a.train_no AND b.stop_seq > a.stop_seq   -- B comes after A
JOIN trains t ON t.train_no = a.train_no
WHERE a.station_code = 'NDLS' AND b.station_code = 'PNBE'
  AND get_bit(t.runs_on_days, extract(isodow FROM DATE '2026-10-01' - a.day_offset)::int - 1) = 1;
  • The index on (station_code, train_no, stop_seq) finds all trains stopping at A and at B quickly, and the join checks the order.
  • runs_on_days + day_offset handles trains that started a day earlier (the date at station A isn't the train's start date).
  • This route-matching result changes rarely, so cache it per (A, B, weekday).

Deep Dive — A seat that is free for part of the journeyDeep dive

A train runs Delhi → Agra → Bhopal → Nagpur. A seat booked Delhi → Agra is free from Agra onward. Availability is not a property of the seat, it is a property of a seat over a range of stops.

Weak

An available flag per seat

Each seat row has a boolean.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  BK["Seat 12B booked Delhi to Agra"] --> FLAG["available = false"]
  FLAG --> Q["Search Bhopal to Nagpur"]
  Q --> HIDE["Seat 12B hidden - it is actually free for that stretch"]
  FLAG --> LOSS["A long-distance train sells a fraction of its real capacity"]

A single flag cannot express partial occupancy, so a seat booked for one short hop is removed from every other search. On a train with many intermediate stops this throws away most of the inventory.

Good

Check for overlapping bookings per seat

Give every stop a sequence number and store each booking as [seq_from, seq_to). A seat is free for A→B if no booking of that seat overlaps that half-open range.

This is the correct model, and half-open ranges make the boundary case fall out cleanly: a booking ending at Agra does not conflict with one starting at Agra. The problem is cost — evaluating it per seat, per class, per train, for every search, on a system where search traffic dwarfs bookings.

Best

Maintain availability counts per segment

Precompute what search actually needs: a count of free seats per (run, class, segment), updated on every booking and cancellation.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  Q["Search A to B, date"] --> RM["Route match - trains stopping at A then later B"]
  RM --> SEGS["Segments spanned: seq_A to seq_B"]
  SEGS --> AV[("availability(run, class, segment) - maintained counts")]
  AV --> MIN["Free seats = min count across the spanned segments"]
  MIN --> RES["Results per class, per quota"]
  BOOK["Booking"] --> DEC["Decrement each spanned segment"]
  CANC["Cancellation"] --> INC["Increment each spanned segment"]
  • The answer is the minimum across the spanned segments. A journey needs the seat free for its whole length, so the bottleneck segment determines availability — and this is a handful of reads instead of a scan over seats.
  • Bookings and cancellations update the counts for the segments they span, which is a small write on a path that is far less frequent than search.
  • Quotas are separate pools. General, Ladies, Tatkal and Senior each carry their own counts, because they are not interchangeable inventory.

Keep the per-seat booking ranges as the source of truth and treat the counts as a derived cache that can be rebuilt. During the Tatkal rush the counts absorb the read storm, while the actual seat assignment still resolves against the authoritative ranges — so a stale count can oversell a search result, never a ticket.

Booking and the Tatkal Rush

  • Booking allocates a specific seat within a transaction: lock the run and class (or the coach), find a seat with no overlapping booking for [seq_A, seq_B), and insert the booking. If none is free, add to the waitlist (WL) or RAC queue.
  • Tatkal at 10:00 AM: a huge spike. Serve search from caches (it can be slightly stale), put booking requests into a queue with fair ordering, and process allocations per train run sequentially (a single writer per run) to avoid lock storms.
  • Search shows "available: ~34" as guidance, and the booking step is authoritative.

Wrap-UpWrap-up

Model stations, trains, ordered train_stops (with day offsets) and per-date train runs. Search by joining stops at A and B where B's sequence comes after A's (indexed by station), plus a running-day check. Track seats as bookings over stop ranges so segments can be reused, keep precomputed availability per class and quota for fast search, and allocate seats transactionally (or via a per-run queue at Tatkal time) with waitlists when full.

More Case Studies

Frequently Asked Questions

What is the Train Search and Seat Availability DB Design (IRCTC) system design question?

Train Search and Seat Availability DB Design (IRCTC) is a system design interview question asked at FAANG companies. It covers databases, booking system, search and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Train Search and Seat Availability DB Design (IRCTC) question?

Flipkart have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Train Search and Seat Availability DB Design (IRCTC) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Train Search and Seat Availability DB Design (IRCTC) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →