Complete System Design Guide

Design Ticket Booking System

Build a platform like BookMyShow handling millions of concurrent bookings

Master seat locking, distributed transactions, flash sale handling, and zero double-booking guarantees

All System Design

What You Will Learn

Problem Statement & Requirements
Scale & Traffic Estimation
High-Level Architecture
Database Schema Design
Seat Selection & Locking
Distributed Locking Strategies
Booking State Machine
Flash Sale Handling
Payment Integration
Real-time Seat Updates
Notification System
Caching & Performance

1Understanding the Problem

A ticket booking system (like BookMyShow, Ticketmaster, or Fandango) allows users to browse events, select specific seats, and complete purchases. The critical challenge is ensuring no two users can book the same seat - a problem that becomes extremely difficult during flash sales when thousands compete for limited inventory.

What Makes Ticket Booking Challenging?

Unlike e-commerce where inventory is fungible (any iPhone is the same), each seat is unique. Two people wanting "Seat A1" creates a direct conflict that must be resolved perfectly.

Unique Inventory

Each seat is one-of-a-kind, no substitutes

Extreme Concurrency

100K+ users competing for same seats

Time-Sensitive

Holds expire, payments can fail

Functional Requirements

  • Browse and search events by category, location, date
  • View venue seat map with real-time availability
  • Select and temporarily hold seats during checkout
  • Complete booking with payment integration
  • Deliver e-tickets via email and mobile app
  • Cancel bookings and handle refunds

Non-Functional Requirements

  • Consistency: Zero double bookings (critical!)
  • Latency: Seat map loads within 2 seconds
  • Scale: 100K+ concurrent users during flash sales
  • Availability: 99.9% uptime during event sales
  • Real-time: Instant seat availability updates
  • Fairness: First-come-first-served ordering

The Golden Rule: A seat can only be sold ONCE. If 1000 users click "Book" on the same seat at the exact same millisecond, exactly ONE should succeed. The other 999 must see "Seat no longer available" - not a double-booking error after payment.

2Scale & Traffic Estimation

Understanding scale helps design appropriate locking and caching strategies.

Scale Assumptions

  • Active Events: 10,000 events at any time
  • Venue Sizes: 100 to 100,000 seats per venue
  • Daily Bookings: 500,000 tickets/day (normal)
  • Registered Users: 50 million
  • Flash Sale Traffic: 100K+ concurrent users per event

Traffic Calculations:

// Normal Day Traffic

500K bookings / 86,400 sec ≈ ~6 bookings/sec

// Flash Sale (Taylor Swift tickets open)

100K users × 10 requests/user in first minute ≈ ~17,000 req/sec

// Seat Availability Queries (read-heavy)

100K users polling every 2 sec = 50,000 reads/sec

// Storage (seats per event avg 10K, 10K events)

10K × 10K × 100 bytes = ~10 GB seat data

Flash Sale Challenge

  • Problem: 100K users, 50K seats → 2:1 oversell ratio
  • First 10 seconds: 80% of bookings attempted
  • Hot seats: Front rows get 1000x more requests
  • Retry storms: Failed users retry aggressively
  • Cart abandonment: 30% don't complete payment

Infrastructure Requirements

  • API Servers: 50+ instances (auto-scale)
  • Database: Read replicas + sharding
  • Redis: Cluster for locks + caching
  • CDN: Static assets (seat maps, images)
  • Queue: Async notification processing

Key Insight: Normal traffic is manageable (~6 TPS), but flash sales create 1000x spikes. The system must handle both gracefully. Design for the peak, optimize for the normal case.

3High-Level Architecture

The architecture separates read-heavy operations (browsing) from write-heavy operations (booking) to scale each independently.

System Architecture Overview


┌─────────────────────────────────────────────────────────────────────────────┐
│                      TICKET BOOKING SYSTEM ARCHITECTURE                      │
└─────────────────────────────────────────────────────────────────────────────┘

         Mobile App              Web App                Admin Portal
              │                     │                        │
              └──────────────┬──────┴────────────────────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │   API Gateway   │ ── Rate Limit, Auth, Load Balance
                    │   (Kong/AWS)    │
                    └────────┬────────┘
                             │
         ┌───────────────────┼───────────────────┐
         ▼                   ▼                   ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│  Event Service  │ │ Booking Service │ │ Payment Service │
│  (Read Heavy)   │ │ (Write Heavy)   │ │   (External)    │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
         │                   │                   │
         │           ┌───────┴───────┐           │
         │           ▼               ▼           │
         │    ┌─────────────┐ ┌─────────────┐   │
         │    │ Lock Service│ │Seat Service │   │
         │    │  (Redis)    │ │             │   │
         │    └─────────────┘ └─────────────┘   │
         │                                       │
         └───────────────────┬───────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                            DATA LAYER                                        │
│                                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    │
│  │  PostgreSQL  │  │    Redis     │  │    Kafka     │  │ Elasticsearch│    │
│  │  (Primary)   │  │  (Cache +    │  │  (Events)    │  │  (Search)    │    │
│  │              │  │   Locks)     │  │              │  │              │    │
│  └──────────────┘  └──────────────┘  └──────────────┘  └──────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                       ASYNC WORKERS                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐                       │
│  │ Notification │  │   Ticket     │  │  Analytics   │                       │
│  │   Worker     │  │  Generator   │  │   Worker     │                       │
│  └──────────────┘  └──────────────┘  └──────────────┘                       │
└─────────────────────────────────────────────────────────────────────────────┘

Core Services

  • Event Service: Manage events, venues, shows, pricing
  • Seat Service: Seat inventory, availability, categories
  • Booking Service: Orchestrate booking flow, state machine
  • Lock Service: Distributed seat locking via Redis
  • Payment Service: Payment gateway integration
  • Notification Service: Email, SMS, push notifications

Data Stores

  • PostgreSQL: Events, bookings, users (ACID)
  • Redis: Seat locks, session, caching
  • Elasticsearch: Event search, filtering
  • Kafka: Event streaming for async ops
  • S3: E-tickets (PDF), venue images
  • CDN: Static assets, seat map SVGs

Separation of Concerns: Read operations (browsing events) can be heavily cached and scaled horizontally. Write operations (booking) require strong consistency and careful coordination. This separation allows each to scale independently.

4Database Schema Design

The schema must efficiently support seat availability queries and prevent double bookings through proper constraints.

Core Tables

-- Venues (theaters, stadiums, etc.)
CREATE TABLE venues (
    venue_id        UUID PRIMARY KEY,
    name            VARCHAR(255) NOT NULL,
    address         TEXT,
    city            VARCHAR(100),
    total_capacity  INT NOT NULL,
    seat_map_url    VARCHAR(500),  -- SVG/JSON for rendering
    created_at      TIMESTAMP DEFAULT NOW()
);

-- Events (movies, concerts, shows)
CREATE TABLE events (
    event_id        UUID PRIMARY KEY,
    venue_id        UUID REFERENCES venues(venue_id),
    title           VARCHAR(255) NOT NULL,
    description     TEXT,
    category        VARCHAR(50),   -- MOVIE, CONCERT, SPORTS, THEATER
    start_time      TIMESTAMP NOT NULL,
    end_time        TIMESTAMP,
    status          VARCHAR(20) DEFAULT 'ACTIVE',  -- ACTIVE, CANCELLED, COMPLETED
    created_at      TIMESTAMP DEFAULT NOW(),
    INDEX idx_venue_time (venue_id, start_time),
    INDEX idx_category_time (category, start_time)
);

-- Seat Categories (VIP, Premium, Regular, etc.)
CREATE TABLE seat_categories (
    category_id     UUID PRIMARY KEY,
    event_id        UUID REFERENCES events(event_id),
    name            VARCHAR(50) NOT NULL,   -- VIP, GOLD, SILVER
    price_cents     BIGINT NOT NULL,
    color_code      VARCHAR(7),             -- For UI rendering
    UNIQUE (event_id, name)
);

-- Individual Seats for each Event
CREATE TABLE event_seats (
    seat_id         UUID PRIMARY KEY,
    event_id        UUID REFERENCES events(event_id),
    category_id     UUID REFERENCES seat_categories(category_id),
    row_label       VARCHAR(10) NOT NULL,   -- A, B, C or 1, 2, 3
    seat_number     INT NOT NULL,
    status          VARCHAR(20) DEFAULT 'AVAILABLE',  -- AVAILABLE, LOCKED, BOOKED
    locked_until    TIMESTAMP,              -- For temporary holds
    locked_by       UUID,                   -- User who locked it
    version         INT DEFAULT 1,          -- Optimistic locking

    UNIQUE (event_id, row_label, seat_number),
    INDEX idx_event_status (event_id, status),
    INDEX idx_locked_until (locked_until) WHERE status = 'LOCKED'
);

-- Bookings
CREATE TABLE bookings (
    booking_id      UUID PRIMARY KEY,
    user_id         UUID NOT NULL,
    event_id        UUID REFERENCES events(event_id),
    status          VARCHAR(20) NOT NULL,   -- PENDING, CONFIRMED, CANCELLED, REFUNDED
    total_amount    BIGINT NOT NULL,
    payment_id      VARCHAR(128),
    booked_at       TIMESTAMP DEFAULT NOW(),
    expires_at      TIMESTAMP,              -- For pending bookings

    INDEX idx_user_bookings (user_id, booked_at DESC),
    INDEX idx_event_bookings (event_id, status)
);

-- Booking Items (seats in a booking)
CREATE TABLE booking_items (
    item_id         UUID PRIMARY KEY,
    booking_id      UUID REFERENCES bookings(booking_id),
    seat_id         UUID REFERENCES event_seats(seat_id),
    price_cents     BIGINT NOT NULL,

    UNIQUE (booking_id, seat_id)
);

Seat Status Flow

AVAILABLE ──select──▶ LOCKED ──pay──▶ BOOKED
     ▲                    │
     │                    │ timeout/cancel
     └────────────────────┘

Status meanings:
• AVAILABLE: Can be selected
• LOCKED: Temporarily held (5-10 min)
• BOOKED: Permanently sold

Key Constraints

  • UNIQUE(event_id, row, seat): Physical uniqueness
  • Version column: Optimistic locking
  • locked_until: Auto-expire holds
  • Index on status: Fast availability queries

5Seat Selection & Locking

When a user selects seats, we need to temporarily hold them while they complete payment. This "lock" prevents others from booking the same seats but must expire if payment isn't completed.

The Seat Lock Problem


Scenario: Two users try to book Seat A1 simultaneously

User 1                           User 2                          Database
   │                                │                                │
   │──── Select Seat A1 ───────────────────────────────────────────▶│
   │                                │──── Select Seat A1 ──────────▶│
   │                                │                                │
   │                                │  WHO GETS THE SEAT?           │
   │                                │                                │

Without Locking: Both see "AVAILABLE", both book → DOUBLE BOOKING!

With Locking:
   │                                │                                │
   │──── Lock Seat A1 ────────────────────────────────────────────▶│
   │◀─── Lock SUCCESS (5 min) ─────────────────────────────────────│
   │                                │──── Lock Seat A1 ────────────▶│
   │                                │◀─── Lock FAILED (already locked)│
   │                                │                                │
   │  (User 1 completes payment)    │  (User 2 sees "Unavailable") │
   │──── Confirm Booking ──────────────────────────────────────────▶│
   │                                │                                │
   │  SEAT A1 = BOOKED             │                                │

Seat Locking Flow


Complete Booking Flow:

┌─────────────────────────────────────────────────────────────────────────┐
│ 1. USER SELECTS SEATS                                                   │
│    • User picks seats: A1, A2, A3                                       │
│    • Frontend sends: POST /api/bookings/hold                            │
└─────────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 2. ACQUIRE LOCKS (Atomic Operation)                                     │
│    • Try to lock ALL selected seats in one transaction                  │
│    • If ANY seat unavailable → Fail entire operation                    │
│    • Set lock_until = NOW() + 10 minutes                                │
│    • Return hold_id and expiry time                                     │
└─────────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 3. USER ON CHECKOUT PAGE                                                │
│    • Timer showing "Complete in 9:45..."                                │
│    • User enters payment details                                        │
│    • If timer expires → Locks released automatically                    │
└─────────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 4. PAYMENT & CONFIRMATION                                               │
│    • Process payment                                                    │
│    • If success: Convert LOCKED → BOOKED                                │
│    • If failure: Release locks, user can retry                          │
│    • Generate e-ticket, send confirmation                               │
└─────────────────────────────────────────────────────────────────────────┘

Lock Acquisition (SQL)

-- Atomic seat locking with optimistic concurrency
UPDATE event_seats
SET
    status = 'LOCKED',
    locked_until = NOW() + INTERVAL '10 minutes',
    locked_by = :user_id,
    version = version + 1
WHERE
    seat_id IN (:seat_ids)
    AND event_id = :event_id
    AND status = 'AVAILABLE'
    AND version = :expected_version
RETURNING seat_id;

-- Check: returned count must equal requested count
-- If not, some seats were not available → rollback

-- Alternative: SELECT FOR UPDATE (pessimistic)
BEGIN;
SELECT * FROM event_seats
WHERE seat_id IN (:seat_ids)
AND status = 'AVAILABLE'
FOR UPDATE NOWAIT;  -- Fail fast if locked

UPDATE event_seats SET status = 'LOCKED' ...
COMMIT;

Lock Release (Expired/Cancelled)

-- Background job: Release expired locks
-- Runs every 30 seconds

UPDATE event_seats
SET
    status = 'AVAILABLE',
    locked_until = NULL,
    locked_by = NULL
WHERE
    status = 'LOCKED'
    AND locked_until < NOW();

-- Manual release (user cancels)
UPDATE event_seats
SET
    status = 'AVAILABLE',
    locked_until = NULL,
    locked_by = NULL
WHERE
    seat_id IN (:seat_ids)
    AND locked_by = :user_id
    AND status = 'LOCKED';

Lock Duration Trade-off: Too short (2 min) → Users can't complete payment. Too long (30 min) → Seats held by abandoned carts. Optimal: 7-10 minutes with a countdown timer shown to users.

6Distributed Locking Strategies

For high-concurrency scenarios, database locks alone may not be fast enough. Redis provides faster distributed locking with automatic expiration.

Optimistic vs Pessimistic Locking

Optimistic Locking

Assume no conflict, verify at commit time using version numbers.

UPDATE seats
SET status = 'LOCKED', version = version + 1
WHERE seat_id = 'A1'
  AND version = 5;  -- Expected version

-- If affected_rows = 0 → Conflict!
  • ✓ No locks held during read
  • ✓ Better for low contention
  • ✗ Retries needed on conflict

Pessimistic Locking

Lock the row immediately, hold until transaction completes.

BEGIN;
SELECT * FROM seats
WHERE seat_id = 'A1'
FOR UPDATE NOWAIT;  -- Lock or fail

UPDATE seats SET status = 'LOCKED';
COMMIT;  -- Release lock
  • ✓ Guaranteed no conflicts
  • ✓ Better for high contention
  • ✗ Can cause deadlocks

Redis Distributed Lock

For flash sales, use Redis as the first line of defense. It's faster than database locks and handles distributed systems well.

// Redis Lock Implementation

async function lockSeats(eventId, seatIds, userId, ttlSeconds = 600) {
    const lockKeys = seatIds.map(id => `lock:event:${eventId}:seat:${id}`);
    const lockValue = `${userId}:${Date.now()}`;

    // Try to acquire all locks atomically using Lua script
    const script = `
        local acquired = {}
        for i, key in ipairs(KEYS) do
            local result = redis.call('SET', key, ARGV[1], 'NX', 'EX', ARGV[2])
            if result then
                table.insert(acquired, key)
            else
                -- Failed to acquire one lock, release all acquired
                for j, acqKey in ipairs(acquired) do
                    redis.call('DEL', acqKey)
                end
                return nil
            end
        end
        return acquired
    `;

    const result = await redis.eval(script, lockKeys.length, ...lockKeys, lockValue, ttlSeconds);

    if (!result) {
        throw new SeatsUnavailableError('One or more seats already locked');
    }

    return { lockValue, expiresAt: Date.now() + (ttlSeconds * 1000) };
}

async function unlockSeats(eventId, seatIds, lockValue) {
    const script = `
        for i, key in ipairs(KEYS) do
            if redis.call('GET', key) == ARGV[1] then
                redis.call('DEL', key)
            end
        end
    `;

    const lockKeys = seatIds.map(id => `lock:event:${eventId}:seat:${id}`);
    await redis.eval(script, lockKeys.length, ...lockKeys, lockValue);
}

Two-Layer Locking Strategy

  1. Layer 1 (Redis): Fast, in-memory lock for immediate feedback
  2. Layer 2 (Database): Persistent lock for durability
  3. Redis lock acquired first → prevents most conflicts
  4. Database lock confirms → ensures data consistency
  5. Both must succeed for a valid hold

Lock Expiration Handling

// Redis TTL auto-expires locks
SET lock:event:123:seat:A1 "user456" EX 600

// Background job syncs DB with Redis
async function syncExpiredLocks() {
    // Find DB locks without Redis locks
    const dbLocks = await db.query(`
        SELECT seat_id FROM event_seats
        WHERE status = 'LOCKED'
        AND event_id = $1
    `);

    for (const seat of dbLocks) {
        const redisKey = `lock:event:123:seat:${seat.id}`;
        const exists = await redis.exists(redisKey);

        if (!exists) {
            // Redis expired, release DB lock
            await releaseSeatInDb(seat.id);
        }
    }
}

Redlock Warning: For critical financial transactions, single Redis instance is sufficient. Redlock (multi-node) adds complexity and has known issues. Keep it simple - use Redis for speed, database for durability.

7Booking State Machine

A booking goes through multiple states. Modeling this explicitly prevents invalid transitions and makes the system behavior predictable.

Booking States & Transitions


Booking State Machine:

                         ┌───────────────────────────────────────────────┐
                         │                                               │
                         │                 INITIATED                     │
                         │           (User selected seats)               │
                         │                                               │
                         └───────────────────────┬───────────────────────┘
                                                 │
                                     seats_locked()
                                                 │
                                                 ▼
                         ┌───────────────────────────────────────────────┐
                         │                                               │
                         │                  PENDING                      │
                         │        (Seats locked, awaiting payment)       │
                         │                                               │
                         └──────────┬─────────────────────┬──────────────┘
                                    │                     │
                        payment_success()            timeout/cancel()
                                    │                     │
                                    ▼                     ▼
         ┌──────────────────────────────────┐  ┌──────────────────────────────────┐
         │                                  │  │                                  │
         │           CONFIRMED              │  │           CANCELLED              │
         │     (Payment successful)         │  │    (Expired or user cancelled)   │
         │                                  │  │                                  │
         └────────────────┬─────────────────┘  └──────────────────────────────────┘
                          │
              user_requests_refund()
                          │
                          ▼
         ┌──────────────────────────────────┐
         │                                  │
         │           REFUNDED               │
         │      (Money returned)            │
         │                                  │
         └──────────────────────────────────┘


State Descriptions:
─────────────────────────────────────────────────────
INITIATED  → Booking created, attempting to lock seats
PENDING    → Seats locked, waiting for payment (TTL: 10 min)
CONFIRMED  → Payment successful, tickets issued
CANCELLED  → Lock expired or user cancelled (seats released)
REFUNDED   → User refunded, seats may or may not be released

State Transition Rules

const VALID_TRANSITIONS = {
    INITIATED: ['PENDING', 'CANCELLED'],
    PENDING:   ['CONFIRMED', 'CANCELLED'],
    CONFIRMED: ['REFUNDED'],
    CANCELLED: [],  // Terminal state
    REFUNDED:  []   // Terminal state
};

function canTransition(from, to) {
    return VALID_TRANSITIONS[from]?.includes(to);
}

async function transitionBooking(bookingId, newStatus) {
    const booking = await getBooking(bookingId);

    if (!canTransition(booking.status, newStatus)) {
        throw new InvalidTransitionError(
            `Cannot transition from ${booking.status} to ${newStatus}`
        );
    }

    await updateBookingStatus(bookingId, newStatus);
    await emitEvent('booking.status_changed', { bookingId, newStatus });
}

Side Effects per Transition

  • INITIATED → PENDING: Lock seats in Redis + DB
  • PENDING → CONFIRMED: Mark seats BOOKED, generate ticket
  • PENDING → CANCELLED: Release locks, free seats
  • CONFIRMED → REFUNDED: Process refund, optionally release seats

8Flash Sale Handling

Flash sales (like Taylor Swift tickets) create extreme load. The system must handle 100K+ concurrent users fairly without crashing.

Flash Sale Challenges

Traffic Spike

100K users hit at exact sale time

Hot Seats

Front rows get 1000x more requests

Retry Storms

Failed users refresh aggressively

Abandoned Carts

30% lock seats but don't pay

Flash Sale Architecture


Flash Sale Traffic Flow:

100K Users
    │
    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         LAYER 1: EDGE/CDN                                    │
│  • Serve static seat map from CDN                                           │
│  • Rate limit: 10 requests/sec per IP                                       │
│  • Queue position page for overflow                                         │
└─────────────────────────────────────────────────────────────────────────────┘
    │ (Filtered to ~50K)
    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                       LAYER 2: VIRTUAL QUEUE                                 │
│  • Users get queue position (You are #4,523)                                │
│  • Admit users gradually (1000/minute)                                      │
│  • Prevents thundering herd on backend                                      │
└─────────────────────────────────────────────────────────────────────────────┘
    │ (Controlled flow ~500/min)
    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                      LAYER 3: BOOKING SERVICE                                │
│  • Redis-first locking (microseconds)                                       │
│  • Optimistic concurrency in DB                                             │
│  • Immediate feedback: success/fail                                         │
└─────────────────────────────────────────────────────────────────────────────┘

Virtual Queue Implementation

// Redis sorted set for queue
async function joinQueue(eventId, userId) {
    const timestamp = Date.now();
    const queueKey = `queue:event:${eventId}`;

    // Add to queue with timestamp as score
    await redis.zadd(queueKey, timestamp, userId);

    // Get position
    const position = await redis.zrank(queueKey, userId);
    const total = await redis.zcard(queueKey);

    return { position: position + 1, total };
}

async function canEnterSite(eventId, userId) {
    const queueKey = `queue:event:${eventId}`;
    const admittedKey = `admitted:event:${eventId}`;

    // Check if already admitted
    if (await redis.sismember(admittedKey, userId)) {
        return true;
    }

    // Get position and admission threshold
    const position = await redis.zrank(queueKey, userId);
    const threshold = await redis.get(`threshold:${eventId}`) || 0;

    return position <= threshold;
}

// Background job: Admit users gradually
async function admitUsers(eventId, batchSize = 100) {
    const threshold = await redis.incr(`threshold:${eventId}`);
    // Notify admitted users via WebSocket
}

Rate Limiting Strategies

  • 1.Per-IP limit: 10 req/sec (prevents bots)
  • 2.Per-user limit: 5 booking attempts/min
  • 3.Per-event limit: Global cap during flash sale
  • 4.CAPTCHA: Trigger after suspicious patterns
  • 5.Queue bypass: Premium members skip queue
# nginx rate limiting
limit_req_zone $binary_remote_addr zone=booking:10m rate=10r/s;
location /api/booking {
    limit_req zone=booking burst=20 nodelay;
}

Fairness Principle: A virtual queue ensures first-come-first-served ordering. Users who clicked at 10:00:00.001 should be ahead of those at 10:00:00.002. This is fairer than "fastest network wins."

9Payment Integration

Payment is the critical path where things can go wrong. Network timeouts, payment failures, and partial completions must all be handled gracefully.

Payment Flow


Payment Integration Flow:

User clicks "Pay Now"
         │
         ▼
┌────────────────────────────────────────────────────────────────────────┐
│ 1. VERIFY LOCKS STILL VALID                                            │
│    • Check Redis locks exist                                           │
│    • Check DB status still PENDING                                     │
│    • If expired: "Sorry, your session timed out"                       │
└────────────────────────────────────────────────────────────────────────┘
         │
         ▼
┌────────────────────────────────────────────────────────────────────────┐
│ 2. CREATE PAYMENT INTENT                                               │
│    • Generate idempotency key: booking_id + attempt                    │
│    • Call payment gateway (Stripe/Razorpay)                            │
│    • Get payment intent ID                                             │
└────────────────────────────────────────────────────────────────────────┘
         │
         ▼
┌────────────────────────────────────────────────────────────────────────┐
│ 3. USER COMPLETES PAYMENT (client-side)                                │
│    • Enter card details                                                │
│    • 3D Secure authentication if required                              │
│    • Payment gateway processes                                         │
└────────────────────────────────────────────────────────────────────────┘
         │
    ┌────┴────┐
    ▼         ▼
SUCCESS    FAILURE
    │         │
    ▼         ▼
┌───────────────┐  ┌───────────────────────────────────────────────────┐
│ 4a. CONFIRM   │  │ 4b. HANDLE FAILURE                                │
│ • Update DB   │  │ • Extend lock by 2 min for retry                  │
│ • Mark BOOKED │  │ • Allow 3 retry attempts                          │
│ • Send ticket │  │ • After 3 fails: Release locks, cancel booking    │
└───────────────┘  └───────────────────────────────────────────────────┘

Idempotency for Payments

// Prevent duplicate charges
async function processPayment(bookingId, paymentDetails) {
    const idempotencyKey = `payment:${bookingId}`;

    // Check if already processed
    const existing = await redis.get(idempotencyKey);
    if (existing) {
        return JSON.parse(existing);  // Return cached result
    }

    // Process payment
    const result = await paymentGateway.charge({
        amount: booking.total_amount,
        idempotency_key: idempotencyKey,
        ...paymentDetails
    });

    // Cache result for 24 hours
    await redis.setex(idempotencyKey, 86400, JSON.stringify(result));

    return result;
}

Timeout Handling

// What if payment times out?
async function handlePaymentTimeout(bookingId) {
    // 1. Check actual status with gateway
    const status = await paymentGateway.getPaymentStatus(bookingId);

    if (status === 'succeeded') {
        // Payment went through despite timeout
        await confirmBooking(bookingId);
    } else if (status === 'failed') {
        // Payment actually failed
        await handlePaymentFailure(bookingId);
    } else if (status === 'pending') {
        // Still processing, wait and retry
        await scheduleStatusCheck(bookingId, { delay: 30000 });
    }
}

// Webhook handler as backup
app.post('/webhooks/payment', async (req, res) => {
    const { event, payment_id, status } = req.body;
    if (event === 'payment.completed') {
        await confirmBookingByPaymentId(payment_id);
    }
});

Critical: Never release seats immediately after payment failure. The payment might still be processing. Always wait for definitive failure confirmation from the payment gateway or timeout after multiple status checks.

10Real-time Seat Updates

Users need to see seat availability change in real-time. When someone books Seat A1, everyone else should see it turn red immediately.

Real-time Architecture


Real-time Seat Availability:

┌─────────────────────────────────────────────────────────────────────────────┐
│                           CLIENT (Browser/App)                               │
│                                                                              │
│   Seat Map Component ←──────── WebSocket Connection ──────────┐             │
│       │                                                       │             │
│       │ Initial load via REST                                 │ Updates     │
│       ▼                                                       │             │
│   GET /api/events/{id}/seats                                  │             │
│       (Returns current availability)                          │             │
│                                                               │             │
└───────────────────────────────────────────────────────────────┼─────────────┘
                                                                │
                                                                │
┌───────────────────────────────────────────────────────────────┼─────────────┐
│                         BACKEND                               │             │
│                                                               │             │
│  Booking Service ──▶ Publish Event ──▶ Redis Pub/Sub ─────────┘             │
│       │                                    │                                │
│       │ On seat status change:             │                                │
│       │ • AVAILABLE → LOCKED              ▼                                │
│       │ • LOCKED → BOOKED          WebSocket Server                        │
│       │ • LOCKED → AVAILABLE       (broadcasts to                          │
│       │                             event subscribers)                      │
│       │                                                                     │
└───────┼─────────────────────────────────────────────────────────────────────┘
        │
        │ Persist
        ▼
   ┌──────────┐
   │ Database │
   └──────────┘

WebSocket Implementation

// Server: Broadcast seat updates
const io = require('socket.io')(server);

io.on('connection', (socket) => {
    socket.on('subscribe:event', (eventId) => {
        socket.join(`event:${eventId}`);
    });
});

// When seat status changes
async function broadcastSeatUpdate(eventId, seatId, newStatus) {
    io.to(`event:${eventId}`).emit('seat:update', {
        seatId,
        status: newStatus,
        timestamp: Date.now()
    });
}

// Client: Listen for updates
socket.on('seat:update', ({ seatId, status }) => {
    updateSeatOnMap(seatId, status);
});

Fallback: Polling

// For clients that can't use WebSocket
// Poll every 5 seconds with ETag

async function pollSeatUpdates(eventId, lastEtag) {
    const response = await fetch(
        `/api/events/${eventId}/seats`,
        { headers: { 'If-None-Match': lastEtag } }
    );

    if (response.status === 304) {
        return null;  // No changes
    }

    const data = await response.json();
    return {
        seats: data.seats,
        etag: response.headers.get('ETag')
    };
}

// Server: Generate ETag from seat versions
const etag = crypto.createHash('md5')
    .update(JSON.stringify(seatVersions))
    .digest('hex');

11Notification & Ticket Delivery

After successful booking, users need confirmation via email, SMS, and push notification. E-tickets must be generated and delivered reliably.

Notification Flow


Booking Confirmed
       │
       ▼
┌─────────────────┐
│  Publish Event  │ ──▶ Kafka topic: booking.confirmed
│  to Queue       │
└────────┬────────┘
         │
         │ Consumers (async, parallel)
         │
    ┌────┼────┬────────────┐
    ▼    ▼    ▼            ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
│Email │ │ SMS  │ │ Push │ │ Ticket   │
│Worker│ │Worker│ │Worker│ │Generator │
└──┬───┘ └──┬───┘ └──┬───┘ └────┬─────┘
   │        │        │          │
   ▼        ▼        ▼          ▼
SendGrid  Twilio   FCM/APNs   PDF Engine
                                  │
                                  ▼
                              S3 Storage
                                  │
                                  ▼
                           Email attachment
                           + In-app download

E-Ticket Generation

  • 1.Generate unique QR code with booking ID
  • 2.Create PDF with event details + QR
  • 3.Store in S3 with signed URL
  • 4.Attach to email + make available in app
  • 5.QR scanned at venue validates against DB

Apple/Google Wallet

  • Generate .pkpass (Apple) / .jwt (Google)
  • Include barcode, event time, venue
  • Auto-surface on lock screen near event time
  • Update pass if event details change

12Caching & Performance

With 50,000 reads/second during flash sales, caching is essential. But cache invalidation for seat availability is tricky.

Caching Strategy by Data Type

Event Details

Title, description, venue info

Cache: 1 hour, CDN

Invalidate on edit

Seat Map Layout

SVG/JSON structure

Cache: 24 hours, CDN

Rarely changes

Seat Availability

Which seats are available

Cache: 2-5 seconds, Redis

Real-time updates via WS

Pricing

Category prices

Cache: 5 minutes, Redis

Invalidate on change

User Session

Auth, cart, locks

Cache: Session TTL, Redis

Stick to user

Search Results

Event listings

Cache: 1 min, Redis

Elasticsearch behind

Read-Through Cache Pattern

async function getSeatAvailability(eventId) {
    const cacheKey = `seats:availability:${eventId}`;

    // Try cache first
    let data = await redis.get(cacheKey);
    if (data) {
        return JSON.parse(data);
    }

    // Cache miss - fetch from DB
    data = await db.query(`
        SELECT seat_id, status
        FROM event_seats
        WHERE event_id = $1
    `, [eventId]);

    // Cache for 5 seconds
    await redis.setex(cacheKey, 5, JSON.stringify(data));

    return data;
}

// Invalidate on any seat change
async function invalidateSeatCache(eventId) {
    await redis.del(`seats:availability:${eventId}`);
}

Database Optimization

  • Read replicas: Serve availability queries
  • Connection pooling: PgBouncer for efficiency
  • Partial indexes: Only AVAILABLE seats
  • Batch queries: Fetch all seats in one query
  • Sharding: By event_id for hot events

Key Design Principles

  • Zero Double Bookings: Use distributed locks (Redis) + database constraints. Both must agree for a valid booking. This is non-negotiable.
  • Temporary Seat Holds: Lock seats for 7-10 minutes during checkout. Auto-release on expiration. Show countdown timer to users.
  • State Machine for Bookings: Explicit states (INITIATED → PENDING → CONFIRMED) with valid transitions prevent invalid operations.
  • Flash Sale Architecture: Virtual queue + rate limiting + caching. Admit users gradually to prevent thundering herd.
  • Real-time Updates: WebSocket for instant seat availability changes. Fallback to polling with ETags for compatibility.
  • Idempotent Payments: Use idempotency keys. Never double-charge even if payment times out and user retries.
  • Async Notifications: Email, SMS, push via message queue. Generate e-tickets asynchronously after booking confirmed.

Ready to Design a Ticket Booking System?

Practice with an AI interviewer and get instant feedback on your system design skills.

Related System Design Questions: