How to Design a Ticket Selling System
Functional Requirements
- Purchase Tickets: Users can buy tickets.
- Refund Tickets: Users can withdraw/refund tickets.
- Select Time: Users can choose the event time.
- Payment Processing: Handle user payments securely.
- Real-time Availability: Users should see the number of remaining tickets.
- Seat Selection: Users can choose their specific seats.
- Two-Phase Purchasing: Ticket buying is split into reservation and payment. If payment isn’t completed within a pre-defined window (e.g., 15 minutes), the reservation is released.
Non-functional Requirements
- High Concurrency: The system must handle a massive spike in read and write traffic immediately after sales open.
- High Availability: The system must remain responsive and usable during peak traffic.
- Consistency: Ensure strong consistency for inventory to prevent over-selling.
Back-of-the-envelope Calculation
Assumptions:
- Total tickets: 100,000
- Active users checking tickets: 5 million
- Time window: 1 minute (traffic spike upon opening)
Read QPS: If requests are evenly distributed over 1 minute:
5,000,000 req / 60 sec ≈ 83,333 req/sec
(Rounded to ~80,000 QPS)
Write QPS: Assuming 80% of active users attempt to buy:
80,000 req/sec * 0.8 = 64,000 req/sec
Peak Traffic: In the first few seconds, traffic could be 5-10x the average, requiring the system to handle significant burst loads.
High-level Design

Design Deep Dive
1. Inventory Deduction
Challenge: With only 1 ticket left and 1,000 requests flooding the queue, how do we efficiently block the 999 failing requests and implement “inventory pre-deduction”?
Solution: When the writing service accepts a request, the pre-deduction should occur in a Redis cache and be queued in the Message Queue (MQ) simultaneously. If an order is cancelled, the inventory must be restored in Redis, and the MQ should be notified to ignore any processing related to that specific cancelled transaction.

2. The Atomicity Challenge
Challenge: Redis GET and DECR operations are not atomic by default. If two users request the last ticket simultaneously, a race condition could lead to over-selling.
Solution: Use Lua scripts in Redis. Lua scripts are executed atomically; even if multiple requests arrive at the same time, Redis executes the script sequentially, ensuring the check-and-decrement logic is safe.
3. The Dual-Write Problem
Challenge: If the service crashes or experiences network issues (network jitter) after updating Redis but before sending the message to the MQ, the inventory data becomes inconsistent (tickets “disappear”).
Solution: Use Transactional Messages (e.g., in RocketMQ). The flow is:
- Prepare: The writing service sends a “half message” to the MQ (invisible to consumers).
- Local Transaction: Execute the Redis Lua script to deduct inventory.
- Commit/Rollback:
- If Redis deduction succeeds, send a Commit to the MQ (making the message visible).
- If it fails, send a Rollback.
- Reconciliation: If the service crashes before step 3, the MQ broker periodically queries the service to check the status of the local transaction and decide whether to commit or roll back.
4. The 15-Minute Release Strategy
Challenge: How to release inventory for orders that remain unpaid after 15 minutes?
Solution: Leverage Delay Queues (available in RocketMQ or via plugins in RabbitMQ).
- When an order is created, the service sends a message with a 15-minute delay to the MQ.
- After 15 minutes, the consumer receives the message.
- The consumer checks the database for the order’s payment status. If unpaid, it cancels the order and restores the inventory.