Choosing a Database for a Ticketing Platform Under Mixed Load
An events company is building a ticketing platform. Two workloads share the same data: (a) a transactional path where users reserve seats, pay, and receive tickets - this must be strongly consistent and handle 5,000 reservations/minute at a flash-sale peak, with zero double-selling; and (b) an analytics path where the marketing team runs ad-hoc queries over 3 years of sales history to find trends, sometimes scanning tens of millions of rows. Today both hit the same relational database. During the last flash sale, analytics queries slowed reservations to the point of timeouts, and a race condition briefly double-sold 12 seats. The team wants one architecture that fixes both problems without doubling operational burden.
1. What is the root cause of the flash-sale failure, and how should the two workloads be separated?
Read the worked answer
Two distinct root causes are being conflated:
- Resource contention: analytics scans (tens of millions of rows) compete with reservation transactions for the same database's CPU/IO, so reservations time out under load. This is a workload isolation problem.
- Double-selling: a race condition in the reservation logic means two transactions both believed a seat was available. This is a concurrency-control problem, independent of load.
Fix both:
- Separate the analytics path from the transactional path. Run analytics against a read replica or a dedicated analytical store fed by replication/CDC, so heavy scans never touch the reservation database.
- Make seat reservation atomic at the database level: enforce a unique constraint or conditional update ("reserve only if status = available") inside a transaction, rather than check-then-write in application code. Optionally use row-level locking or an optimistic version column.
The judgement: isolation fixes the timeouts; atomicity fixes the double-sell. Treating them as one problem leads to the wrong fix.
2. What database characteristics matter most for the reservation path, and what should the team avoid?
Read the worked answer
The reservation path needs a database that offers:
- Strong consistency / ACID transactions - non-negotiable for zero double-selling. A seat is a scarce resource; eventual consistency can oversell.
- Low, predictable latency at 5,000 reservations/minute (~83/sec sustained, with burstier spikes) - the engine must handle short write transactions with row-level locking efficiently.
- Unique constraints and conditional updates as first-class features to enforce atomicity.
- Read scaling via replicas for seat-availability display, without weakening the write path.
Avoid:
- Putting analytics on the same instance (the original mistake).
- Using an eventually-consistent store as the source of truth for seat allocation - it invites oversell.
- Application-level check-then-write for availability - the source of the race.
A relational engine with ACID guarantees and a replica for reads is the safe default; a specialized high-concurrency store is acceptable only if it provides the needed atomic conditional writes.
3. How should the team validate the fix before the next flash sale?
Read the worked answer
Validate with a concurrency-focused load test that reproduces contention and races, not just throughput:
- Replay the mixed load: drive 5,000 reservations/minute while simultaneously running heavy analytics scans, to confirm isolation actually protects the transactional path (measure reservation p99 latency and error rate).
- Adversarial concurrency test: many clients attempt to reserve the same seat simultaneously; assert exactly one succeeds and the rest get a clean "unavailable" response. Run this at high concurrency to shake out races.
- Verify analytics isolation: confirm scans hit the replica/analytical store and that replication lag stays acceptable for the freshness the marketing team needs.
- Failover drill: kill the primary and confirm the reservation path fails over without double-selling or data loss.
- Capacity headroom: test above peak to find the breaking point.
The judgement: a load test that only measures throughput would have passed the broken system. The test must assert correctness under concurrency and isolation under mixed load, because those were the actual failures.