Flash Sale & Ticketing Platform (Ticketmaster)
1. Problem Statement & Scope
Designing a ticketing platform for a massive stadium tour is one of the most uniquely hostile environments in software engineering. Unlike a standard e-commerce platform where inventory gradually depletes over weeks, a highly anticipated concert triggers an instantaneous, deliberate Denial of Service (DoS) attack from your own legitimate customers. Two million humans and millions of automated scalper bots will hit your servers at exactly 10:00:00 AM.
Functional Requirements
- Discovery: Users can view interactive seating charts and real-time seat availability.
- Reservation (The Temporary Hold): Users can click a specific seat and lock it exclusively for 10 minutes while they navigate the payment flow.
- Booking: Users successfully pay and receive a cryptographically verifiable ticket.
Non-Functional Requirements
- Absolute Consistency: Zero tolerance for double-booking. The inventory is a strict physical constraint.
- Extreme Concurrency: The system must gracefully survive a Thundering Herd of traffic at the exact millisecond the sale opens.
- Fairness & Bot Mitigation: The system must prevent scalper networks from vacuuming up the inventory in the first 3 seconds.
2. Back-of-the-Envelope Estimation
- Traffic Spike: 2,000,000 concurrent users at exactly 10:00 AM.
- Read QPS: ~2,000,000 requests/second at peak (must be absorbed almost entirely by the CDN/Edge).
- Write QPS: Only 50,000 tickets exist, so successful disk writes are low. However, attempted lock acquisitions will exceed 100,000+ QPS.
- Storage: 50,000 tickets $\times$ 1KB per record $\approx$ 50MB per event.
- The Reality Check: Storage size is irrelevant here. The entire architectural challenge revolves around compute bottlenecks, network I/O, and distributed lock contention in memory.
3. High-Level Design (HLD)
To survive the traffic spike, we must construct a series of defensive perimeters, ensuring the primary database is shielded behind edge caches, asynchronous queues, and in-memory lock managers.
4. Deep Dive: Component Design
The Virtual Waiting Room & Edge Defenses
If 2 million users hit your backend application servers simultaneously, they will exhaust your database connection pools in milliseconds. The database will thrash, queries will queue, and the entire stack will return 502 Bad Gateway errors.
We push the defense to the network edge. When users arrive at 10:00 AM, the WAF (Web Application Firewall) intercepts them. We deploy Proof-of-Work (PoW) challenges to burn scalper bot CPU cycles. Legitimate requests are assigned a cryptographically signed cryptographic token and dropped into a highly distributed message broker like Apache Kafka.
The backend application servers consume from this Kafka topic at a strictly controlled rate—say, 5,000 users per second. The remaining 1.99 million users are served a lightweight, static HTML polling page from the CDN. The backend systems are completely unaware of the 1.99 million waiting users, operating comfortably at their maximum safe throughput.
Caching Strategy & Distributed Locking
Once a user is let through, they view the seating chart and select Seat A1. User B, who was also just let through, clicks Seat A1 at the exact same millisecond.
If we check the relational database (SELECT status FROM seats WHERE id = 'A1'), both users see the seat as available. We cannot use standard pessimistic database locks (SELECT ... FOR UPDATE) because locking thousands of rows concurrently will instantly bottleneck the SQL engine.
Instead, we use a single-threaded in-memory datastore: Redis.
When a user clicks “Checkout”, the App Server fires an atomic Redis command:
SET lock:event123:seatA1 user_id NX PX 600000
NX: Only set the key if it Not eXists.PX 600000: Set a Time-To-Live (TTL) of 600,000 milliseconds (10 minutes).
Because Redis is single-threaded, it perfectly serializes the incoming requests. User A’s command executes first, acquiring the lock. User B’s command executes a microsecond later, fails because the key already exists, and the application immediately tells User B the seat is taken.
Question to ponder: If Redis is single-threaded, doesn’t it become the new bottleneck? How do you partition the Redis cluster to distribute the lock requests? If you hash by event_id, all 100,000 requests for the pop star still hit a single Redis node. You must shatter the hot key by hashing event_id:section_id.
Database Schema & Partitioning
Once the temporary lock is acquired in Redis, the user has 10 minutes to pay. Only upon successful payment do we persist the final state to the permanent ledger.
We use a relational database (PostgreSQL) for strict ACID compliance.
- Events:
event_id(PK),name,date. - Seats:
seat_id(PK),event_id(Partition Key),status. - Bookings:
booking_id(PK),user_id,event_id,seat_id.
To prevent the PostgreSQL writer node from melting during massive tours, we must horizontally shard the database. As discovered with Redis, sharding purely by event_id is a fatal mistake—it isolates the entire concert onto one physical disk. We must use a Composite Shard Key, such as hash(event_id + section_id), spreading the writes for a single stadium evenly across multiple database clusters.
5. Fault Tolerance & Edge Cases
The Redis Node Crash
What happens if the primary Redis node crashes exactly after granting User A the lock, but before replicating that lock to its read-replica? The cluster promotes the replica, User B requests a lock for Seat A1, and the new primary grants it because it never received the replication log.
We now have a double-booking. For a true ticketing system, standard Redis asynchronous replication is insufficient. We must discuss deploying the Redlock Algorithm, which requires acquiring locks from a quorum (majority) of independent Redis nodes before proceeding, trading latency for absolute consistency.
The Dead Client and The Saga Pattern
User A acquires the lock, but their laptop battery dies before they pay.
The system does not need a background cron job to clean this up. The PX 600000 command means Redis will automatically evict the lock in exactly 10 minutes, making the seat instantly available to the next user.
However, what if the user submits their credit card, our server contacts the Payment Gateway, the bank deducts the money, but the HTTP response drops due to a network partition? Our server doesn’t know if the payment succeeded.
We must implement a Distributed Saga. The transaction state is saved as PENDING. A background Reconciliation Engine continuously sweeps the database for stale pending transactions, querying the downstream Payment Gateway’s API to explicitly verify the settlement status. If the payment failed, the worker rolls back the local reservation. If it succeeded, it finalizes the ticket.
Question to ponder: What happens if the Payment Gateway’s verification API goes down for 4 hours during your 10-minute flash sale? Do you extend the Redis locks indefinitely and freeze the inventory, or do you release the seats and risk refunding angry customers later? In distributed systems, technology eventually ends, and business policy must take over.