System Design · Interview Deep Dive

Design BookMyShow: Movie Ticket Booking System Design

Two lakh fans hit 'Book' the minute advance booking opens, and no two of them may ever get seat F12. Seat holds with TTL, atomic locking, payment timeouts, idempotency and the waiting room that keeps it all standing.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 18 min read
Cinema seat map with locked gold seats and hourglass timers, and a queue of people at a virtual gate
TL;DR
  • BookMyShow is a contended inventory problem: thousands of users race for the same few hundred seats per show, and a seat may be sold exactly once.
  • Booking is a two-phase flow: hold seats with a TTL (a few minutes) → pay → confirm. Holds expire automatically; confirmation is a conditional state change.
  • Prevent double booking with atomic all-or-nothing holds (conditional UPDATE in one transaction, or a Redis Lua script) plus a unique constraint on (show, seat) for confirmed tickets.
  • Handle late payments (re-acquire or auto-refund), and make create-booking, payment and webhooks idempotent.
  • Survive release-day spikes with a virtual waiting room, cached catalogue, in-memory seat maps sharded by show, and bot protection.

Most system design questions are about scale. BookMyShow is about correctness under contention, with scale arriving in sudden spikes. On a normal Tuesday, a city's theatres sell seats at a gentle pace. When advance booking opens for a big-star release or a cricket World Cup screening, a large crowd lands on the same shows within minutes, all wanting the same middle rows. The system must never sell one seat twice, never take money without issuing a ticket, and must stay up while doing it.

Interviewers at Indian product companies love this question because it is familiar and because the weak answer ("check if the seat is free, then book it") contains a race condition you can expose in one follow-up. Let us build the strong answer.

Requirements

Functional

  • Browse movies by city, see theatres and showtimes, view a live seat map with prices per seat category.
  • Select up to 10 seats and hold them for a fixed window while paying (UPI, cards, wallets, net banking).
  • On successful payment, confirm and issue an e-ticket (QR code) by app, SMS, email and WhatsApp.
  • Cancel bookings where the theatre allows it, with refunds.
  • Admin/partner side: theatres publish screens, layouts, shows and prices.

Non-functional

  • No double booking (strong consistency for seat inventory).
  • No lost money: every successful payment ends as a ticket or a refund.
  • High availability for browsing; booking may queue users under extreme load but should not fail randomly.
  • Low latency for seat map and hold (under ~300 ms).
  • Fairness during flash demand; resistance to bots and scalpers.

Capacity Estimation

Interview assumptions, sized for India:

QuantityAssumption / mathResult
Screens on the platformIndia has roughly 9,000–10,000 screens; assume 8,000 listed8,000
Shows per screen per dayAssumption5
Shows per day8,000 × 540,000
Seats per showAverage~200
Seat inventory per day40,000 × 2008M seats
Show-seat rows live (7-day booking window)8M × 756M rows
Tickets per day (normal / big release)Assumption1M / 4M
Browse-to-book ratioAssumption~50:1

Average load is modest: 1M tickets ÷ 86,400 ≈ 12 bookings/s, ~600 browse requests/s. The design is driven by peaks. Say booking opens at 10:00 for a blockbuster's first weekend, and 300,000 users arrive in the first five minutes: 1,000 users/s, each loading a few pages and polling the seat map, so 10,000+ requests/s concentrated on a few hundred shows. A popular 10 pm Saturday show at a premium multiplex might have thousands of users trying to hold its ~250 seats within seconds. That single show is a hot partition, and it is the real design target.

Storage is small: 56M show-seat rows × ~100 bytes ≈ 6 GB; bookings at ~1 KB × 1M/day ≈ 1 GB/day. A sharded relational database handles this comfortably. Contention, not volume, is the problem.

~12/s
Avg bookings
10K+/s
Release-day peak requests
~56M
Live show-seat rows
1
Sales allowed per seat

The Booking Flow and Seat State Machine

  1. Browse: city → movie → theatre and showtime (cached, read-heavy).
  2. Seat map: layout (static, cached) + live availability (fast store).
  3. Hold: user selects F11, F12 and taps Book. Server atomically moves both seats AVAILABLE → HELD with an expiry (for example 8–10 minutes) and creates a booking in PENDING_PAYMENT.
  4. Pay: redirect to the payment gateway or UPI intent; a countdown shows remaining hold time.
  5. Confirm: on payment success (webhook or client callback, verified server-side), atomically move seats HELD → BOOKED and booking PENDING_PAYMENT → CONFIRMED, then issue the ticket.
  6. Expire or fail: if payment fails or the hold expires, seats return to AVAILABLE and the booking becomes EXPIRED or FAILED.
Seat: AVAILABLE ──hold──► HELD ──payment ok──► BOOKED ▲ │ │ └──expiry/cancel───┘ └──cancel+refund──► AVAILABLE Booking: PENDING_PAYMENT ──► CONFIRMED ──► CANCELLED ──► REFUNDED │ ├──► EXPIRED (hold timed out, no payment) ├──► FAILED (payment declined) └──► REFUND_PENDING (paid, but seats lost: late payment)

API Design

HTTP API
GET  /v1/cities/{city}/movies
GET  /v1/movies/{movie_id}/shows?city=BLR&date=2026-10-17
GET  /v1/shows/{show_id}/seatmap
     → { layout_version, categories:[{name:"Recliner", price:650}...],
         unavailable:["F11","F12","G7", ...] }            # HELD or BOOKED

POST /v1/shows/{show_id}/holds
     headers: Idempotency-Key: 3b6f0c1e-...
     body: { seats:["F11","F12"] }
     → 201 { booking_id, hold_expires_at, amount: 1300 }
     → 409 { error:"SEATS_UNAVAILABLE", seats:["F12"] }

POST /v1/bookings/{booking_id}/payments
     headers: Idempotency-Key: 9a1d...
     body: { method:"upi" }   → { payment_id, gateway_redirect / upi_intent }

POST /v1/webhooks/payment-gateway       # signed by gateway, verified server-side
GET  /v1/bookings/{booking_id}          # status polling for the client
POST /v1/bookings/{booking_id}/cancel

Database Schema

SQL (PostgreSQL-flavoured)
CREATE TABLE theatre   (theatre_id BIGINT PRIMARY KEY, city_id INT, name TEXT, chain TEXT);
CREATE TABLE screen    (screen_id BIGINT PRIMARY KEY, theatre_id BIGINT REFERENCES theatre,
                        name TEXT, layout_json JSONB, layout_version INT);
CREATE TABLE movie     (movie_id BIGINT PRIMARY KEY, title TEXT, language TEXT,
                        duration_min INT, certificate TEXT);
CREATE TABLE show      (show_id BIGINT PRIMARY KEY, screen_id BIGINT REFERENCES screen,
                        movie_id BIGINT REFERENCES movie, starts_at TIMESTAMPTZ,
                        status TEXT);              -- SCHEDULED, OPEN, CLOSED, CANCELLED

CREATE TABLE show_seat (
  show_id         BIGINT NOT NULL,
  seat_id         TEXT   NOT NULL,                 -- 'F12'
  category        TEXT   NOT NULL,
  price_paise     INT    NOT NULL,                 -- money in integer paise
  status          TEXT   NOT NULL DEFAULT 'AVAILABLE',   -- AVAILABLE | HELD | BOOKED
  booking_id      BIGINT,
  hold_expires_at TIMESTAMPTZ,
  version         INT    NOT NULL DEFAULT 0,
  PRIMARY KEY (show_id, seat_id)
);

CREATE TABLE booking (
  booking_id      BIGINT PRIMARY KEY,
  user_id         BIGINT NOT NULL,
  show_id         BIGINT NOT NULL,
  seats           TEXT[] NOT NULL,
  amount_paise    INT    NOT NULL,
  status          TEXT   NOT NULL,               -- PENDING_PAYMENT, CONFIRMED, EXPIRED...
  hold_expires_at TIMESTAMPTZ,
  idempotency_key TEXT   NOT NULL,
  created_at      TIMESTAMPTZ DEFAULT now(),
  UNIQUE (user_id, idempotency_key)
);

-- final safety net: a seat of a show can appear in only one confirmed ticket
CREATE TABLE ticket_seat (
  show_id BIGINT, seat_id TEXT, booking_id BIGINT NOT NULL,
  PRIMARY KEY (show_id, seat_id)
);

CREATE TABLE payment (
  payment_id     BIGINT PRIMARY KEY,
  booking_id     BIGINT NOT NULL,
  gateway        TEXT, gateway_txn_id TEXT,
  amount_paise   INT, status TEXT,                -- INITIATED, SUCCESS, FAILED, REFUNDED
  UNIQUE (gateway, gateway_txn_id)                -- webhook dedup
);

Design notes: store money as integer paise, never floats. Shard show_seat and booking by show_id (or by city, since shows never cross cities) so every hold is a single-shard transaction. Seat layouts are static per screen and belong in a cache/CDN, not in per-request queries.

High-Level Architecture

Users ──► CDN (posters, layouts, static) ──► Waiting Room (release days) │ admitted users only ▼ API Gateway (auth, rate limits, bot checks) ┌──────────────────┬───────────────┼─────────────────┬────────────────┐ ▼ ▼ ▼ ▼ ▼ Catalogue Svc Seat Map Svc Booking Svc Payment Svc Notification (movies, shows; (availability (holds, confirm, (gateway, UPI, (SMS, email, search index + from Redis) expiry) webhooks, WhatsApp, QR) cache) │ │ │ reconciliation) ▲ ▼ ▼ │ │ │ Redis (per-show Booking DB (sharded │ │ seat state, by show_id: show_seat, │ │ hold TTLs) booking, ticket_seat) ◄──┘ │ │ │ └── outbox ──► Kafka ───────────────┘ └─► expiry sweeper, analytics, partner sync

Deep Dive 1: Seat Locking with TTL

The naive implementation is a race:

Request A: SELECT status FROM show_seat WHERE seat='F12'  → AVAILABLE
Request B: SELECT status FROM show_seat WHERE seat='F12'  → AVAILABLE
Request A: UPDATE show_seat SET status='HELD' ...          (A thinks it won)
Request B: UPDATE show_seat SET status='HELD' ...          (B thinks it won too)

Check-then-act must be a single atomic step. Three ways to do it:

Option 1: conditional UPDATE in a transaction (recommended baseline)

SQL: all-or-nothing hold
BEGIN;
UPDATE show_seat
   SET status = 'HELD', booking_id = :booking_id,
       hold_expires_at = now() + interval '8 minutes', version = version + 1
 WHERE show_id = :show_id
   AND seat_id = ANY(:seats)                       -- e.g. {'F11','F12'}
   AND (status = 'AVAILABLE'
        OR (status = 'HELD' AND hold_expires_at < now()));   -- reclaim expired holds
-- application checks: rows_updated == array_length(:seats)
-- if not equal: ROLLBACK (someone took at least one seat) → 409 to client
INSERT INTO booking (...) VALUES (... 'PENDING_PAYMENT' ...);
COMMIT;

The WHERE status = 'AVAILABLE' clause makes the update its own lock check: under PostgreSQL or MySQL InnoDB, concurrent updates to the same row serialise on the row lock, and the second one re-evaluates the condition after the first commits, finds the seat HELD, and updates zero rows. Rolling back when fewer rows than requested were updated gives all-or-nothing semantics, so a user never ends up with F11 but not F12. To avoid deadlocks when two users request overlapping seat sets in different orders, always lock seats in a consistent order (sort seat IDs). Isolation-level nuances are explained in ACID transactions and isolation levels.

Notice expiry is lazy: an expired hold is simply treated as available by the next hold attempt. A background sweeper still runs every few seconds to flip expired holds back to AVAILABLE and mark bookings EXPIRED, so the seat map looks right and users get "your hold expired" notifications, but correctness never depends on the sweeper running on time.

Option 2: pessimistic locking

SELECT … FOR UPDATE on the requested seats, check status in application code, then update. Correct, but it holds row locks across an application round trip, which hurts under heavy contention. Prefer the conditional update.

Option 3: Redis holds for hot shows

For extreme contention, keep live seat state for open shows in Redis and do holds there with a Lua script (atomic per show when the show's keys share a hash slot):

Lua: atomic multi-seat hold with TTL
-- KEYS = seat keys, e.g. {show:9912}:F11, {show:9912}:F12   (same hash slot)
-- ARGV[1] = booking id, ARGV[2] = hold ms
for i, key in ipairs(KEYS) do
  if redis.call('EXISTS', key) == 1 then
    return {0, key}                     -- some seat already held or booked
  end
end
for i, key in ipairs(KEYS) do
  redis.call('SET', key, ARGV[1], 'PX', tonumber(ARGV[2]))
end
return {1}

Booked seats are stored without expiry; held seats expire on their own. The database remains the durable record: after the Redis hold succeeds, the booking row is written; on confirmation, ticket_seat rows are inserted. If Redis loses data in a failover, the database's unique constraint still prevents double sale (see next section), and Redis can be rebuilt from the database.

Java: in-memory model of the hold/confirm logic
import java.util.*;

public final class ShowSeatInventory {
    enum Status { AVAILABLE, HELD, BOOKED }

    private static final class Seat {
        Status status = Status.AVAILABLE;
        String holdId;
        long holdExpiresAt;
    }

    private final Map<String, Seat> seats = new HashMap<>();
    private final long holdMillis;

    public ShowSeatInventory(Collection<String> seatIds, long holdMillis) {
        for (String id : seatIds) seats.put(id, new Seat());
        this.holdMillis = holdMillis;
    }

    private static boolean isFree(Seat s, long now) {
        return s.status == Status.AVAILABLE
            || (s.status == Status.HELD && s.holdExpiresAt <= now);   // lazy expiry
    }

    /** All-or-nothing hold. Returns a hold id, or null if any seat is unavailable. */
    public synchronized String hold(List<String> seatIds, long now) {
        for (String id : seatIds) {
            Seat s = seats.get(id);
            if (s == null || !isFree(s, now)) return null;
        }
        String holdId = UUID.randomUUID().toString();
        for (String id : seatIds) {
            Seat s = seats.get(id);
            s.status = Status.HELD;
            s.holdId = holdId;
            s.holdExpiresAt = now + holdMillis;
        }
        return holdId;
    }

    /** Confirm after payment: only if every seat is still held by this hold and unexpired. */
    public synchronized boolean confirm(String holdId, List<String> seatIds, long now) {
        for (String id : seatIds) {
            Seat s = seats.get(id);
            if (s.status != Status.HELD || !holdId.equals(s.holdId) || s.holdExpiresAt <= now) {
                return false;                       // caller must re-acquire or refund
            }
        }
        for (String id : seatIds) {
            Seat s = seats.get(id);
            s.status = Status.BOOKED;
            s.holdId = null;
        }
        return true;
    }

    public static void main(String[] args) {
        long tenMin = 10 * 60 * 1000;
        ShowSeatInventory show = new ShowSeatInventory(List.of("F10", "F11", "F12", "F13"), tenMin);
        String asha = show.hold(List.of("F11", "F12"), 0);
        String ravi = show.hold(List.of("F12", "F13"), 1_000);           // F12 taken
        System.out.println((asha != null) + " " + (ravi == null));       // true true
        String meera = show.hold(List.of("F12", "F13"), tenMin + 1);      // Asha's hold expired
        System.out.println(meera != null);                                // true
        System.out.println(show.confirm(asha, List.of("F11", "F12"), tenMin + 5)); // false: refund
    }
}

Dry run: Asha holds F11–F12 at t = 0 (expires at 10 minutes). Ravi asks for F12–F13 at t = 1 s; F12 is held, so the whole request fails and F13 is not held either. At t = 10 min + 1 ms Asha's hold has expired, so Meera's request for F12–F13 succeeds. When Asha's payment finally succeeds at t = 10 min + 5 ms, confirmation fails because F12 now belongs to Meera's hold, so Asha's booking must go to the refund path. Every branch of the real system is visible in these 50 lines.

Deep Dive 2: Guaranteeing No Double Booking

Defence in depth, from the user's tap to the ticket:

  • Atomic hold (conditional update or Lua) so only one hold can own a seat at a time.
  • Conditional confirm: UPDATE show_seat SET status='BOOKED' WHERE show_id=? AND seat_id = ANY(?) AND status='HELD' AND booking_id=?, and confirm only if all rows updated. An expired-and-reclaimed hold fails this check.
  • Unique constraint: inserting into ticket_seat with primary key (show_id, seat_id) in the same transaction. Even if a bug or a Redis failover lets two confirmations through, the second insert violates the key and the transaction rolls back. Constraints are the last line of defence that application bugs cannot bypass.
  • Single writer per show shard: all writes for a show go to its primary; never accept holds on replicas.

Deep Dive 3: Payment Timeouts and Late Payments

Payments are where the hard edge cases live, especially with UPI where the user may approve in their bank app a minute or two later, or the gateway's webhook arrives late.

ScenarioHandling
Payment succeeds within holdConditional confirm; issue ticket; publish event via outbox
Payment failsBooking FAILED; release seats immediately (do not wait for TTL)
User abandonsHold expires; sweeper marks EXPIRED; seats back to AVAILABLE
Payment succeeds after hold expiredTry to re-acquire the same seats atomically; if free, confirm; else mark REFUND_PENDING and auto-refund, notify the user
Payment status unknown (gateway timeout)Booking stays PAYMENT_PENDING; reconciliation job polls the gateway's status API; never assume failure without confirmation
Webhook arrives twiceDedup on (gateway, gateway_txn_id) unique key; confirm is idempotent

Two refinements reduce late-payment refunds. First, when the user initiates payment, extend the hold once by a short grace period (or set status PAYMENT_IN_PROGRESS with its own longer expiry). Second, have the client stop accepting new payment attempts in the last minute of the hold. The reconciliation job is essential: it compares the gateway's settlement records with your bookings daily, catching anything webhooks missed. The broader money-movement patterns are in payment system and UPI design, and the multi-step confirm/refund flow is a classic saga.

Deep Dive 4: Idempotency Everywhere

Mobile networks drop responses; users double-tap "Pay"; gateways retry webhooks. Without idempotency you create duplicate bookings and double charges.

  1. Client generates an Idempotency-Key (UUID) per logical action (one per hold attempt, one per payment attempt) and reuses it on retries.
  2. Server stores the key with the result: UNIQUE (user_id, idempotency_key) on booking. A retry with the same key returns the original booking instead of creating a new one. If the first request is still in progress, return 409/425 "in progress" and let the client poll.
  3. Payment requests carry the booking ID as the merchant order ID, and gateways generally reject or dedupe duplicate order IDs.
  4. Webhook handling is idempotent: insert into payment with the unique gateway transaction ID; on conflict, do nothing and return 200.
  5. State transitions are conditional (WHERE status = 'PENDING_PAYMENT'), so processing "payment success" twice changes nothing the second time.
  6. Side effects via outbox: the confirm transaction writes an outbox row; a relay publishes it to Kafka for ticket emails/SMS. Consumers dedupe by booking ID, so a user never gets five copies of the ticket.

Deep Dive 5: Flash Demand on Release Days

When booking opens for a huge release, demand can be orders of magnitude above normal for a few minutes. Strategies, outermost first:

  • Virtual waiting room. Users hitting the movie's booking page get a queue token and a position; a gatekeeper admits them at a rate the booking tier can sustain (say 2,000 users/minute), issuing a signed, short-lived admission token checked at the API gateway. Fairer than random failures, and it turns a spike into a steady flow. IRCTC's Tatkal window faces the same problem; compare with IRCTC Tatkal design.
  • Pre-scale. Release dates are known; scale API, booking and Redis capacity up before booking opens. Warm caches for the movie, its shows and seat layouts.
  • Cache aggressively. Movie pages, theatre lists, showtimes and layouts are static for hours; serve them from CDN and cache. Only availability is dynamic.
  • Seat map from memory. Keep per-show availability in Redis (a hash or bitmap of seat → status) updated on every hold/confirm/expiry. The seat map endpoint never touches the database. Clients poll every few seconds or receive pushes, and stale maps are acceptable because the hold itself is authoritative ("Sorry, F12 was just taken").
  • Shard by show. Hot shows spread across shards; a single ultra-hot show is still one shard, but one show is only a few hundred seats, so even thousands of hold attempts per second on one row set are manageable with short transactions or Redis.
  • Rate limits and bot defence. Per-user and per-device hold limits, maximum seats per booking, CAPTCHA or device attestation at the waiting room, and detection of scripted traffic. Scalpers are a real problem for big events. See rate limiter design.
  • Graceful degradation. Turn off non-essential features (recommendations, reviews widgets) during the spike to free capacity.

The same playbook (queue at the edge, in-memory inventory, atomic decrements, async order processing) is applied to flash sales like Big Billion Days; the difference is that seats are distinct items with positions, while sale stock is a fungible counter.

Deep Dive 6: Integrating with Theatre Systems

An aggregator often does not own the inventory. Large chains and many single screens run their own ticketing/POS systems and sell at the box office and on their own apps too. That changes the design:

  • The theatre system is the source of truth for seats. A hold must be placed through the partner's API (block seats with their own timeout), and confirmation is a second partner call (sell/commit).
  • Partner APIs are slower and less reliable than your own database. Wrap them with timeouts, retries with idempotency (partner booking references), and circuit breakers.
  • The flow becomes a saga: local hold → partner block → payment → partner commit → local confirm; compensations are partner release and refund. Partner commit failure after payment means refund.
  • Availability for seat maps is synced from partners (push or frequent pull) and cached, accepting brief staleness.

Mentioning this unprompted is a strong senior signal: it shows you have thought about where the data actually lives, not just about an idealised single database.

What Interviewers Look For

  • You spot the check-then-act race and fix it with an atomic conditional update, then add a unique constraint as a safety net.
  • You use holds with TTL and lazy expiry rather than cron-dependent correctness.
  • You handle late payments, unknown payment status and duplicate webhooks explicitly.
  • You make every mutating call idempotent.
  • You size for peaks, not averages, and bring a waiting room, caching and bot defence.

Follow-ups to prepare

  • "Users want to pick seats that are next to each other automatically." Best-available algorithm over the layout: score contiguous runs by row preference and centrality, then attempt an atomic hold; retry the next best if it fails.
  • "How do you show a live seat map to 5,000 people watching one show?" Push deltas over WebSockets or SSE from a per-show channel, or poll a cached bitmap every 2–3 seconds.
  • "How do you support group bookings across two shows?" Separate holds per show, combined in one saga with all-or-nothing compensation.
🚫
Common mistakesRead-then-write without atomicity. Holding seats forever because cleanup depends on a cron job. Locking the whole show (or the whole theatre) for every booking. Distributed locks with no TTL. Treating payment gateway timeouts as failures and releasing seats while the money is still in flight. Using floating-point money. Ignoring bots on release day.

Hotel and homestay booking adds date ranges on top of the same ideas; once you are comfortable here, try Airbnb / hotel booking design.

Frequently Asked Questions

How does BookMyShow prevent two people from booking the same seat?▾
The seat hold is an atomic state change: a conditional update that moves seats from available to held only if they are still available, inside one transaction, or an equivalent atomic Redis script. Only one request can win. A unique constraint on show and seat for confirmed bookings acts as a final safety net in the database.
Why are seats held for a few minutes during booking?▾
Payment takes time, especially with UPI approvals, OTPs and bank redirects. Holding the chosen seats with an expiry gives the user a guaranteed window to pay, while the TTL ensures abandoned carts release seats automatically without a human or a cleanup bug blocking inventory forever.
Should seat locks be stored in Redis or the database?▾
Either can work. The database with conditional updates is simpler and keeps one source of truth. Redis with SET NX and TTL, or a Lua script for multi-seat holds, absorbs much higher contention during flash demand. A common design uses Redis for fast holds and the database as the durable record of confirmed bookings with a uniqueness constraint.
What happens if payment succeeds after the seat hold expires?▾
The system tries to re-acquire the same seats; if they are still free, the booking is confirmed. If someone else has taken them, the booking fails and the payment is refunded automatically. Many systems also extend the hold once a payment is in progress to make this case rare.
What is an idempotency key in a booking system?▾
An idempotency key is a unique client-generated ID sent with a request such as create booking or pay. If the request is retried because of a timeout or a double tap, the server recognises the key and returns the original result instead of creating a second booking or charging twice.
How do ticketing sites handle huge traffic when bookings open?▾
They put a virtual waiting room in front of the booking flow that admits users at a controlled rate, pre-scale capacity, serve static content from CDNs and caches, keep seat availability in fast in-memory stores, rate limit per user and device, and block bots with challenges.
What database is best for a movie ticket booking system?▾
A relational database with ACID transactions is the natural fit for seat inventory, bookings and payments, because correctness depends on atomic multi-row updates and constraints. It can be sharded by show or by city, while read-heavy catalogue data is cached or served from a search index.

Prepare for Booking-System Interviews

Prepflix's system design course covers BookMyShow, IRCTC and flash-sale designs with concurrency deep dives and mock interviews that test exactly these edge cases.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.