Loading...
āœ“

12-Hour Money-Back Guarantee

šŸŽŸļøDesigning Event Booking / Seat Reservation System Part 1- Database Modelling

šŸŽŸļøDesigning Event Booking / Seat Reservation System Part 1- Database Modelling

šŸŽŸļøDesigning Event Booking / Seat Reservation System Part 1- Database Modelling

3 Apr 20223 min read

Understanding the Problem

Core Requirements

  • One seat can be booked by only one user

  • Thousands of users may attempt booking simultaneously

  • Seat must be temporarily held during payment

  • Holds must expire automatically

  • Booking must be atomic and consistent

Solutions

1ļøāƒ£ Naive Design (Why It Fails)

Naive Table

CREATE TABLE Seat (
    seat_id BIGINT PRIMARY KEY,
    event_id BIGINT,
    status VARCHAR(20) -- AVAILABLE / BOOKED
);

Booking Logic (āŒ Wrong)

SELECT status FROM Seat WHERE seat_id = 101;
-- if AVAILABLE
UPDATE Seat SET status = 'BOOKED' WHERE seat_id = 101;

Problems

āŒ Race conditions
āŒ Double booking
āŒ No payment timeout
āŒ No concurrency control

2ļøāƒ£ Correct Mental Model

Booking is not a single step.

It is a state machine:

AVAILABLE → HELD → BOOKED

With timeouts and ownership.

3ļøāƒ£ Optimized Core Data Model

šŸŽÆ Seat Table (Static)

CREATE TABLE Seat (
    seat_id BIGINT PRIMARY KEY,
    event_id BIGINT,
    seat_number VARCHAR(10)
);

šŸŽÆ Seat Reservation (Dynamic State)

CREATE TABLE SeatReservation (
    seat_id BIGINT,
    user_id BIGINT,
    status VARCHAR(20), -- HELD / BOOKED / EXPIRED
    hold_expires_at DATETIME,
    PRIMARY KEY (seat_id),
    INDEX (user_id)
);

Why Separate Table?

  • Seat = metadata

  • Reservation = state

  • Easier expiration & cleanup

4ļøāƒ£ Seat Hold (Critical Concurrency Control)

Atomic Seat Hold

INSERT INTO SeatReservation (seat_id, user_id, status, hold_expires_at)
VALUES (?, ?, 'HELD', NOW() + INTERVAL 5 MINUTE);

āœ” Primary key on seat_id
āœ” Only one insert succeeds
āœ” Others fail → seat already held

The database becomes the arbiter of truth.

5ļøāƒ£ Booking Confirmation (After Payment)

UPDATE SeatReservation
SET status = 'BOOKED'
WHERE seat_id = ?
  AND user_id = ?
  AND status = 'HELD'
  AND hold_expires_at > NOW();

Guarantees

āœ” Seat still held
āœ” Hold not expired
āœ” Correct user

6ļøāƒ£ Hold Expiry (Auto Cleanup)

Option A — Background Job (Most Common)

UPDATE SeatReservation
SET status = 'EXPIRED'
WHERE status = 'HELD'
  AND hold_expires_at < NOW();

Option B — Redis TTL (High Scale)

SET seat:101:user:7 HELD EX 300

7ļøāƒ£ Preventing Overselling at Scale

šŸ”’ Strategy A — DB Uniqueness (Baseline)

  • PK on seat_id

  • Simple

  • Reliable

⚔ Strategy B — Redis Lock (High Throughput)

SETNX lock:seat:101 userId EX 5
  • Fast

  • Distributed

  • Must handle crash recovery

8ļøāƒ£ Read Optimization (Seat Map)

Seat Availability Query

SELECT s.seat_id,
       CASE
         WHEN r.status = 'BOOKED' THEN 'BOOKED'
         WHEN r.status = 'HELD' AND r.hold_expires_at > NOW() THEN 'HELD'
         ELSE 'AVAILABLE'
       END AS status
FROM Seat s
LEFT JOIN SeatReservation r ON s.seat_id = r.seat_id
WHERE s.event_id = ?;

Cache This (Redis)

event:42:seatmap

9ļøāƒ£ High-Level Architecture

Failure Scenarios

Scenario Handling
Payment fails Let hold expire
User abandons TTL cleanup
App crash DB still correct
Redis down DB uniqueness saves
Retry request Idempotent update