Build a platform like BookMyShow handling millions of concurrent bookings
Master seat locking, distributed transactions, flash sale handling, and zero double-booking guarantees
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.
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
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.
Understanding scale helps design appropriate locking and caching strategies.
500K bookings / 86,400 sec ≈ ~6 bookings/sec
100K users × 10 requests/user in first minute ≈ ~17,000 req/sec
100K users polling every 2 sec = 50,000 reads/sec
10K × 10K × 100 bytes = ~10 GB seat data
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.
The architecture separates read-heavy operations (browsing) from write-heavy operations (booking) to scale each independently.
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
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.
The schema must efficiently support seat availability queries and prevent double bookings through proper constraints.
-- 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)
);AVAILABLE ──select──▶ LOCKED ──pay──▶ BOOKED
▲ │
│ │ timeout/cancel
└────────────────────┘
Status meanings:
• AVAILABLE: Can be selected
• LOCKED: Temporarily held (5-10 min)
• BOOKED: Permanently soldWhen 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.
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 │ │
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 │
└─────────────────────────────────────────────────────────────────────────┘
-- 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;-- 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.
For high-concurrency scenarios, database locks alone may not be fast enough. Redis provides faster distributed locking with automatic expiration.
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!
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
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);
}// 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.
A booking goes through multiple states. Modeling this explicitly prevents invalid transitions and makes the system behavior predictable.
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
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 });
}Flash sales (like Taylor Swift tickets) create extreme load. The system must handle 100K+ concurrent users fairly without crashing.
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 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 │
└─────────────────────────────────────────────────────────────────────────────┘
// 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
}# 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."
Payment is the critical path where things can go wrong. Network timeouts, payment failures, and partial completions must all be handled gracefully.
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 │
└───────────────┘ └───────────────────────────────────────────────────┘
// 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;
}// 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.
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 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 │
└──────────┘
// 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);
});// 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');After successful booking, users need confirmation via email, SMS, and push notification. E-tickets must be generated and delivered reliably.
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
With 50,000 reads/second during flash sales, caching is essential. But cache invalidation for seat availability is tricky.
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
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}`);
}Practice with an AI interviewer and get instant feedback on your system design skills.