πŸ“š Study Notes / Home / HLD / Session 16
Session 16 Β· Case Study β€” IRCTC Finale

Designing IRCTC: the whole course, in one system

This is the grand finale. IRCTC β€” India's railway ticketing platform β€” is the perfect capstone because it needs everything we've studied at once: load balancing, caching, sharding, replication, queues, and rock-solid consistency under brutal traffic spikes. We'll start from zero (an everyday story), build the full architecture piece by piece, then revisit the genuinely hard parts and finish with an interview-ready checklist. Take it slow β€” by the end you'll be able to design a system like this on a whiteboard with confidence.

⏱ 23 min readπŸ“– 4 topics

1 Putting it all together


Explain like I'm 5

Imagine the busiest train station in the world. At 10:00 every morning, a special "fast tickets" window opens, and millions of people rush the counter at the exact same second β€” all wanting the same few seats. You need lots of clerks (so no one waits forever), a board showing which seats are left (so people don't ask the same question over and over), many copies of the seat ledger (so it never gets lost), and β€” most important β€” a rule that the same seat is never sold to two people. IRCTC is exactly that station, built in software.

IRCTC (Indian Railway Catering and Tourism Corporation) runs the online booking system for Indian Railways. It is one of the most demanding systems on Earth, and it stresses almost every idea from this subject simultaneously. Before we draw a single box, let's frame why each tool we learned is forced into the design.

The five forces that shape the design

Force / requirementWhy IRCTC has itTool we reach for
Huge, spiky read & write trafficTatkal (last-minute) booking opens at fixed times; lakhs of users hit "search" and "book" within seconds.Load balancing & horizontal scaling (Sessions 2–4).
Repeated, identical readsEveryone searches the same popular trains/routes; train schedules barely change.Caching (Sessions 6–7).
More data & writes than one DB can holdCrores of bookings, passengers, and PNRs across thousands of trains.Sharding / partitioning (Sessions 9–10).
Must never lose a booking & survive failuresA confirmed ticket cannot vanish if a server dies.Replication & durability (Sessions 11–12).
Absolutely no double-selling a seatOne berth = one passenger. Money is involved.Strong consistency & concurrency control (Sessions 8, 13).
The big idea

No single technique saves IRCTC. The skill of high-level design is choosing the right combination β€” and accepting the trade-offs each choice forces. Caching makes reads fast but can go stale. Sharding adds capacity but complicates transactions. Replication adds durability but risks reading old data. The art is putting them together so the system is fast and correct and survives failure.

The tension at the heart of it: speed vs. correctness

Recall the CAP theorem (Session 8): under a network partition you can't have both perfect consistency and full availability. IRCTC is unusual because different parts of it want different things:

  • Search & availability display can be slightly stale β€” "approximately 12 seats left" is fine. This part leans toward availability (AP).
  • The actual seat allocation & payment must be exactly right β€” you cannot "approximately" sell a seat. This part demands strong consistency (CP).

Recognising that one system can mix consistency models per-component is the single most important insight of this whole case study. Keep it in your back pocket.

Concrete example: the same seat, two users

At 10:00:00.000, User A and User B both see "Seat S7 available" (a cached/read view). Both click Book. If the system naively trusts the read, both get S7 β€” a disaster. The fix isn't "make search perfectly consistent" (too slow for millions). The fix is: search can be approximate, but the final commit goes through a strongly-consistent path that lets exactly one of them win and tells the other "sorry, just gone." Speed where it's safe, correctness where it counts.

A back-of-envelope sense of scale

IRCTC reportedly handles on the order of 10+ lakh (a million+) bookings a day, with Tatkal windows pushing 10,000–25,000+ ticket requests per minute at peak, and far more search traffic than that. Reads vastly outnumber writes β€” a classic ratio (often 10:1 or higher) that we'll exploit heavily with caching. We'll do proper capacity estimation in Section 4.

Recap IRCTC needs load balancing (spiky traffic), caching (repeated reads), sharding (too much data/writes), replication (durability + failure survival), and strong consistency (no double-booking) β€” all at once. The key move is mixing consistency models per component: keep search fast and approximate, keep seat allocation + payment strongly consistent.

2 End-to-end architecture


Explain like I'm 5

Think of a giant restaurant. A greeter at the door sends you to a free table (load balancer). Specialist chefs each do one job β€” one makes salads, one makes desserts (microservices). There's a notepad of popular orders so the kitchen doesn't re-cook the same thing (cache). The pantry is split across several rooms so it never runs out of shelf space (sharding), and every important recipe is copied into a backup binder (replication). Slow tasks like "send the bill by email" get dropped into an out-tray someone clears later (a queue). Our whole architecture is just this restaurant, drawn carefully.

Let's build the full picture. We'll go left-to-right, from the user's phone to the database and back, and at each box explain why it's there, referencing the session that taught the idea.

The request's journey, top to bottom

πŸ“±
Clients
App / web / agents
β†’
🌐
CDN + DNS
Static assets, GeoDNS
β†’
🚦
Load balancer
+ API gateway
β†’
🧩
Microservices
Search, Booking, Payment…
β†’
⚑
Cache
Redis cluster
β†’
πŸ—„οΈ
DB
Sharded + replicated
β†’
πŸ“¨
Queue
Async jobs & notify

Component by component

1. Clients & the edge (CDN + DNS)

Users arrive from the mobile app, the website, or third-party travel agents (which hit the API directly). Static, rarely-changing content β€” JavaScript bundles, CSS, station logos, schedule PDFs β€” is served by a CDN (Content Delivery Network): edge servers physically close to users (Session 6). GeoDNS points each user at the nearest healthy region. This keeps slow, repetitive downloads off the core servers entirely.

2. Load balancer + API gateway

All dynamic traffic hits a load balancer (Session 3–4) that spreads requests across many identical application servers so no single machine is overwhelmed. In front of (or merged with) it sits an API gateway that handles cross-cutting concerns: TLS termination, authentication, rate limiting (crucial for Tatkal β€” Session 5), and routing each request to the right microservice.

Which load-balancing algorithm?

Recall from Session 4: round-robin is simple and fine when servers are equal and requests are uniform. Least-connections is better when some requests are slow (a booking holds a connection longer than a search). IRCTC-style traffic usually picks least-connections (or latency-aware) at L7, with health checks so dead servers are pulled out automatically.

3. Stateless microservices

Instead of one giant program, the logic is split into focused microservices, each independently deployable and scalable:

ServiceResponsibilityScaling profile
Search / AvailabilityFind trains, show seats-left. Read-heavy.Scale out a lot; cache aggressively.
BookingHold a seat, create a PNR, coordinate the transaction. The correctness core.Scale, but guard with locks/transactions.
PaymentTalk to payment gateways; confirm or reverse.Reliability over raw speed; idempotent.
User / AuthLogin, profile, saved passengers.Moderate; cacheable sessions.
NotificationSMS/email/push of tickets & updates.Fully async via queue.

The golden rule: keep services stateless (no user data stored in the server's local memory). Any server can handle any request, so the load balancer can freely add/remove machines. State lives in the cache and database, not the app server.

4. Cache layer (Redis cluster)

A distributed in-memory cache such as Redis sits between services and the database (Sessions 6–7). It stores hot, repeated data so reads don't pound the DB:

  • Train schedules & route info (changes rarely β†’ long TTL).
  • Search results / fare lookups (short TTL, e.g. a few seconds).
  • Approximate seat-availability counters (very short TTL or event-updated).
  • Sessions, rate-limit counters, and Tatkal queue tokens.

5. Database β€” sharded and replicated

This is the heart. We need a relational database (e.g. PostgreSQL/MySQL) for transactional correctness on bookings and payments β€” recall ACID transactions from Session 8. One database can't hold the load, so we apply two orthogonal techniques learned in Sessions 9–12:

  • Sharding (horizontal partitioning): split data across many DB servers. A natural shard key is train_id (or train_id + journey_date) so that all seats for one train on one day live together β€” letting a single booking transaction stay inside one shard.
  • Replication: each shard has a primary (handles writes) and one or more replicas (handle reads + take over on failure). This gives durability, read-scaling, and failover.

6. Queues & the notification pipeline

Slow or non-critical work is pushed onto a message queue (e.g. Kafka/RabbitMQ β€” Session 14/15) and processed asynchronously. After a booking commits, we drop a "ticket booked" event on the queue; downstream consumers send the SMS, email the e-ticket, update analytics, and notify waitlist logic β€” none of which the user has to wait for.

The full component diagram

                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   πŸ“± Clients ──▢ β”‚  CDN + DNS  β”‚  (static assets, GeoDNS)
 (app/web/agent) β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                        β”‚ dynamic requests
                        β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  Load Balancer +   β”‚  TLS, auth, rate-limit
              β”‚   API Gateway      β”‚  (Sessions 3–5)
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό               β–Ό               β–Ό               β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Search  β”‚    β”‚ Booking  β”‚    β”‚ Payment  β”‚    β”‚  Auth /  β”‚   β—€ stateless
   β”‚ Service β”‚    β”‚ Service  β”‚    β”‚ Service  β”‚    β”‚  User    β”‚     microservices
   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
        β”‚   reads      β”‚ read+write    β”‚                β”‚
        β–Ό              β–Ό               β”‚                β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚     Redis cache cluster   β”‚β—€β”€β”€β”€β”€β”€β”€β”˜          β”‚ sessions β”‚
   β”‚  (schedules, availability)β”‚                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚ on miss / for writes
                β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚           Sharded SQL database                β”‚
   β”‚  shard key = train_id (+ journey_date)        β”‚
   β”‚                                               β”‚
   β”‚  Shard A         Shard B         Shard C      β”‚
   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
   β”‚  β”‚primary β”‚      β”‚primary β”‚      β”‚primary β”‚   β”‚ β—€ writes
   β”‚  β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜   β”‚
   β”‚   replicas        replicas        replicas    β”‚ β—€ reads + failover
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚ "booking confirmed" event
                            β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  Message queue    β”‚  (Kafka / RabbitMQ)
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β–Ό             β–Ό             β–Ό
          πŸ“¨ SMS        βœ‰οΈ Email     πŸ“Š Analytics
        (async notification consumers)
Worked example: a single search request

User searches "NDLS β†’ BCT, 25 Dec":

  • Request hits the load balancer β†’ routed to a free Search service instance.
  • Search builds a cache key like avail:NDLS-BCT:2025-12-25 and checks Redis. Cache hit (likely, popular route) β†’ returns in <5 ms. Done; the DB was never touched.
  • On a cache miss, Search reads from a read replica of the relevant shard, returns the result, and writes it back to Redis with a short TTL (e.g. 5 s) so the next million identical searches are cheap.

Notice: a read-heavy request never touched a write primary and usually never touched the DB at all. That's how the system survives millions of searches.

Worked example: a booking request (the write path)

User clicks "Book S7 on train 12951":

# Simplified booking flow (pseudocode)
def book_seat(train_id, date, seat, user):
    shard = route_to_shard(train_id)        # sharding: all seats for this train live here
    with shard.transaction():            # strong consistency on the write path
        row = SELECT status FROM seats
              WHERE train_id=train_id AND date=date AND seat=seat
              FOR UPDATE               # row lock: nobody else can grab S7 now
        if row.status != "AVAILABLE":
            raise SeatTaken                # the loser is told politely
        UPDATE seats SET status="HELD", held_by=user
        pnr = create_pnr(...)
    enqueue("start_payment", pnr)         # payment kicked off; queue + idempotency
    return pnr

The write goes to the primary of one shard, inside a transaction with a row lock β€” exactly one booker can win S7. We dig into this in Section 3.

Recap Clients β†’ CDN/DNS β†’ load balancer + API gateway β†’ stateless microservices (Search, Booking, Payment, Auth, Notification) β†’ Redis cache β†’ sharded + replicated SQL database β†’ message queue β†’ async notifications. Reads are absorbed by the cache and replicas; writes go through the primary of one shard inside a transaction; slow work goes async. Every box maps to an idea from an earlier session.

3 The hard parts revisited


Explain like I'm 5

The easy part is drawing boxes. The hard part is the four "uh oh" moments: two kids grabbing the same toy at once, a thousand kids rushing the gate the instant it opens, paying for a toy but the shop crashing before it's wrapped, and a sign that says "5 toys left" when there are really 2. Every serious system lives or dies on how it handles these. Let's solve each one carefully.

A clean diagram doesn't make a system work β€” the details do. Here are the four genuinely hard problems in IRCTC, the design decision for each, and the trade-off you accept.

Hard part A β€” Double-booking (concurrency control)

The nightmare: two users buy the same berth. This is a classic race condition (Session 13). The fix is concurrency control β€” making sure that for any one seat, bookings are effectively serialised. Two families of approaches:

ApproachHow it worksBest whenTrade-off
Pessimistic lockingSELECT … FOR UPDATE locks the seat row; others wait or fail.High contention (Tatkal β€” everyone wants the same seats).Locks reduce concurrency; risk of lock waits / deadlocks.
Optimistic lockingRead a version number; on update, check it didn't change (WHERE version = v); retry if it did.Low contention; many distinct seats.Wasted work under high contention (lots of retries).
Worked example: optimistic vs. pessimistic on seat S7
-- PESSIMISTIC: take the lock up front (good under Tatkal contention)
BEGIN;
SELECT status FROM seats
  WHERE train_id=12951 AND date='2025-12-25' AND seat='S7'
  FOR UPDATE;                 -- row locked; the other txn blocks here
UPDATE seats SET status='HELD' WHERE ...;
COMMIT;

-- OPTIMISTIC: no lock; detect the clash at write time
UPDATE seats SET status='HELD', version=version+1
  WHERE seat='S7' AND status='AVAILABLE' AND version=42;
-- if 0 rows updated β†’ someone beat us β†’ retry or report "seat gone"

Because Tatkal is extreme contention on a few seats, IRCTC-style designs lean pessimistic (or a short-lived distributed lock / atomic counter), keeping the locked section tiny so locks are held for milliseconds.

The "hold" trick

Real booking adds a step: when you start booking, the seat is moved to HELD with a short timer (e.g. you have 10 minutes to pay). If you don't pay, a background job releases it back to AVAILABLE. This prevents seats being locked forever by people who abandoned checkout β€” at the cost of needing a timeout/cleanup mechanism.

Hard part B β€” Tatkal traffic spikes

At the Tatkal window, traffic jumps maybe 10–50Γ— in seconds. You cannot keep that many servers running 24/7. The toolkit:

  • Autoscaling + pre-warming: scale out app servers before the known window (you know exactly when it opens β€” pre-provision capacity).
  • Rate limiting & throttling (Session 5) at the gateway: cap requests per user/IP to stop bots and protect the core.
  • A virtual waiting room / queue: admit users in controlled batches instead of letting all of them hit the booking DB at once β€” turning a stampede into an orderly line.
  • Load shedding: if overwhelmed, reject excess requests fast with a clear message rather than letting everything slow to a crawl (a slow system that serves no one is worse than a fast "try again").
  • Cache & CAPTCHA the read-heavy parts so only genuine booking writes reach the DB.
Trade-off

A waiting room and rate limits frustrate users ("why am I in a queue?") β€” but the alternative is the whole system collapsing for everyone. Controlled fairness beats uncontrolled failure. This is a deliberate availability decision.

Hard part C β€” Payment consistency

The scary scenario: money leaves the user's account but no ticket is issued β€” or a ticket is issued but the payment failed. The booking and the payment live in different systems (your DB vs. an external payment gateway), so a single ACID transaction can't span both. The tools:

  • Idempotency keys: every payment attempt carries a unique key so retries don't charge twice. Critical, because mobile networks drop and clients retry constantly.
  • The Saga pattern (Session 14): model the booking as a sequence of steps, each with a compensating action. If payment fails after the seat was HELD, the compensation releases the seat. If the ticket can't be issued after payment succeeded, the compensation triggers a refund.
  • An outbox + reconciliation: record intended actions durably and have a background job reconcile against the gateway, so nothing is silently lost.
Worked example: a booking saga
πŸ”’
1. Hold seat
seat β†’ HELD
β†’
πŸ’³
2. Charge
idempotent payment
β†’
🎫
3. Confirm
seat β†’ BOOKED, issue PNR

If step 2 fails β†’ compensate step 1 (release seat). If step 3 fails after step 2 succeeded β†’ compensate step 2 (refund). The system reaches a correct end state even though no single transaction covered everything. Trade-off: sagas give eventual consistency and need careful compensation logic β€” but they're the realistic way to coordinate across services and external gateways.

Hard part D β€” Search / availability caching

Search is read-heavy and must be fast, so we cache aggressively. But availability changes as seats sell β€” so the cache goes stale. The design decision is to accept staleness on the display, but never on the commit:

  • Show a cached, slightly-stale count ("~12 available", short TTL). Cheap and fast.
  • Re-check the true availability inside the strongly-consistent transaction at booking time (Section 2's FOR UPDATE). The cache can lie; the transaction cannot.
  • Use cache invalidation / event-driven updates: when a seat is booked, publish an event so caches refresh sooner.
Watch out: the thundering herd

When a hot key (popular train) expires, thousands of requests miss the cache simultaneously and stampede the DB β€” the thundering herd / cache stampede problem (Session 7). Mitigate with request coalescing (only one request rebuilds the key while others wait), slightly randomised TTLs (jitter), and serving stale-while-revalidate.

Recap Four hard parts, four decisions: (A) double-booking β†’ locking (pessimistic under Tatkal contention) + short HELD timers; (B) spikes β†’ pre-warmed autoscaling, rate limiting, a virtual waiting room, load shedding; (C) payments β†’ idempotency + the saga pattern with compensations + reconciliation; (D) search caching β†’ tolerate stale display but verify truth in the transaction, and guard against cache stampedes. Each fixes a real problem and costs something β€” naming the trade-off is the whole job.

4 Scaling, reliability & interview tips


Explain like I'm 5

Before building a bridge, engineers count how many cars will cross and where it might break, then add backups so it doesn't fall down. We do the same for software: estimate the traffic (capacity), find the part most likely to snap (bottleneck), plan for failures, and β€” in an interview β€” explain all of this out loud in a calm, organised way. This section is your toolkit and your script.

Capacity estimation (back-of-the-envelope)

Interviewers love this. You won't be exact β€” they want to see structured reasoning. A worked pass for IRCTC:

Worked estimate
  • Bookings/day: ~1,200,000 (round to 1M+). Spread over a busy ~12 h β†’ ~1,200,000 / (12 Γ— 3600) β‰ˆ ~28 writes/sec average.
  • Peak (Tatkal) factor ~50Γ— β†’ ~1,400 booking writes/sec at peak. Plan headroom for ~2,000–3,000/sec.
  • Reads (search) ≫ writes. At, say, 30 searches per booking β†’ average ~850 reads/sec, peaking into the tens of thousands/sec. This is why caching is non-negotiable.
  • Storage: 1.2M bookings/day Γ— ~2 KB/record β‰ˆ ~2.4 GB/day β‰ˆ ~0.9 TB/year of booking rows (plus passengers, logs, indexes). Comfortably needs sharding over a few years.
  • Bandwidth: dominated by search responses; offloaded heavily to CDN + cache.

The takeaway numbers: writes are modest but spiky; reads are enormous; storage grows steadily. That single sentence justifies the entire architecture.

Bottleneck analysis

Always ask "what breaks first?" For IRCTC the answer is almost always the write path to the booking database during Tatkal β€” because writes need consistency and can't just be cached. Mitigations, in order of leverage:

BottleneckSymptomMitigation
Booking DB write contentionLock waits, timeouts at 10:00Shard by train; short transactions; waiting room to flatten the spike.
Cache stampede on hot keysDB CPU spikes when a key expiresRequest coalescing, TTL jitter, stale-while-revalidate.
Single hot shard (one super-popular train)One shard maxed, others idleFiner shard key (train+date), or replicate that shard's reads.
Payment gateway latencyThreads tied up waitingAsync/saga, timeouts, circuit breakers.

Failure handling & reliability

  • No single point of failure (SPOF): redundant load balancers, multiple app instances, replica DBs that can be promoted on primary failure (automatic failover, Session 12).
  • Replication for durability: a committed booking exists on a primary and replicas, so one disk dying never loses a ticket.
  • Circuit breakers & timeouts: if the payment gateway is down, fail fast and stop hammering it β€” don't let one slow dependency take down everything.
  • Graceful degradation: if search is struggling, serve cached/stale results rather than erroring. Read features can survive even if some write features are paused.
  • Idempotency everywhere on the write path so retries are safe.
  • Monitoring & alerting: watch latency, error rate, queue depth, lock waits β€” you can't fix what you can't see.

How to present this in a system-design interview

The biggest mistake is diving into a database schema in minute one. Use this order β€” it's the same order this page followed, which is no accident:

❓
1. Clarify
requirements & scope
β†’
πŸ”’
2. Estimate
capacity / traffic
β†’
🧱
3. High-level
draw the boxes
β†’
πŸ”¬
4. Deep dive
the hard parts
β†’
βš–οΈ
5. Trade-offs
& failure modes
Interview checklist

Tick these off out loud and you'll cover what interviewers grade:

  • β˜‘ Functional requirements: search trains, check availability, book, pay, cancel, view PNR.
  • β˜‘ Non-functional: high availability for search, strong consistency for booking, handle spikes, low latency.
  • β˜‘ Capacity estimate: reads ≫ writes; writes spiky; storage grows β†’ state it explicitly.
  • β˜‘ High-level diagram: clients β†’ LB/gateway β†’ services β†’ cache β†’ sharded+replicated DB β†’ queue.
  • β˜‘ Data model & shard key: seats keyed by train_id + date; justify the choice.
  • β˜‘ The hard parts: double-booking (locking), spikes (rate limit + waiting room), payment (saga + idempotency), caching (stale display, true commit).
  • β˜‘ Consistency story: AP for search, CP for booking β€” say it clearly (CAP, Session 8).
  • β˜‘ Failure handling: no SPOF, replication/failover, circuit breakers, graceful degradation.
  • β˜‘ Trade-offs named: for every choice, what you gained and what it cost.
  • β˜‘ Bottleneck & future scaling: identify the booking-write path; how you'd grow it.
Common pitfalls to avoid

Don't claim "I'll just cache everything" (booking writes can't be cached). Don't forget idempotency on payments. Don't propose strong consistency for everything (you'll never hit the throughput). Don't go silent β€” narrate your reasoning. And always state trade-offs; an answer with no trade-offs reads as naΓ―ve.

Recap Estimate capacity (reads ≫ writes, spiky writes, growing storage); find the bottleneck (the booking-write path during Tatkal); design for failure (no SPOF, replication/failover, circuit breakers, graceful degradation, idempotency, monitoring); and present in the order clarify β†’ estimate β†’ high-level β†’ deep-dive β†’ trade-offs, narrating throughout. The checklist is your safety net.

β˜… Putting it all together & course finale


You just designed one of the hardest real-world systems there is β€” and in doing so you used every major idea from this subject. Here's the one-paragraph story that ties this capstone together:

IRCTC must be fast, correct, and survive failure all at once. Requests flow from clients through a CDN and a load balancer + API gateway into stateless microservices. A Redis cache absorbs the enormous read traffic; a sharded (by train) and replicated SQL database holds the truth and survives machine death; a message queue carries slow work like notifications asynchronously. The design's soul is mixing consistency models: search is AP (fast, slightly stale), but seat allocation and payment are CP β€” protected by locking, the saga pattern with idempotency, and short HELD timers, so a seat is never sold twice and money is never lost. Spikes are tamed with autoscaling, rate limiting, and a virtual waiting room; failures are contained with replication/failover, circuit breakers, and graceful degradation. Name the trade-off behind every choice, and you've given a senior-level answer.

πŸŽ‰ You finished the entire HLD course!

Congratulations β€” this was the final session, and you made it all the way through high-level system design. Look back at the journey you took:

  • Scaling foundations β€” vertical vs. horizontal scaling, statelessness, and why we add machines instead of bigger machines.
  • Load balancing β€” spreading traffic with round-robin / least-connections, health checks, and API gateways.
  • Caching β€” CDNs, Redis, cache strategies, TTLs, invalidation, and the stampede problem.
  • Storage & replication β€” SQL vs. NoSQL, sharding/partitioning, primary–replica replication, durability, and failover.
  • Consistency & NoSQL internals β€” CAP, ACID vs. BASE, concurrency control, sagas, and how distributed databases really behave.
  • Case studies β€” culminating here, where you wove all of it into one coherent system.

That arc β€” scaling β†’ load balancing β†’ caching β†’ storage/replication β†’ NoSQL internals β†’ case studies β€” is exactly the mental model you'll reuse for any system-design problem. You now have it. Go draw some boxes with confidence. πŸš†

Quick self-check

Why can search tolerate stale data but booking cannot?

Showing "~12 seats left" when it's really 11 is harmless β€” the user just sees an approximate count. But selling the same physical seat to two people is a hard failure involving money. So search is AP (fast, cacheable, slightly stale) while booking is CP (strongly consistent, transactional). One system, two consistency models.

What's a good shard key for IRCTC seats, and why?

train_id (often combined with journey_date). It keeps all seats for one train on one day inside a single shard, so a booking transaction stays local to one shard β€” no slow cross-shard distributed transaction needed.

Under heavy Tatkal contention, do you prefer optimistic or pessimistic locking?

Pessimistic. When everyone fights over the same few seats, optimistic locking causes endless retries (wasted work). A short-held pessimistic lock (or atomic counter / distributed lock) serialises access cleanly. Keep the locked section tiny so locks last milliseconds.

The booking succeeds but the payment fails. How do we stay correct?

Use the saga pattern: each step has a compensating action. If payment fails after the seat was HELD, the compensation releases the seat. With idempotency keys, retries don't double-charge, and a reconciliation job ensures nothing is silently lost.

What is the thundering herd / cache stampede, and how do you prevent it?

When a hot cache key expires, thousands of requests miss simultaneously and stampede the DB. Prevent it with request coalescing (one rebuild while others wait), randomised TTLs (jitter), and stale-while-revalidate (serve old data while refreshing).

In a system-design interview, what should you do before drawing any boxes?

Clarify requirements (functional + non-functional) and do a capacity estimate. Only then draw the high-level diagram, deep-dive the hard parts, and discuss trade-offs and failure modes. Narrate your reasoning throughout.

πŸ“š References & Further Reading


Class material

Papers, docs & deep dives