Courses › System Design › Patterns
Dealing with Contention
Pessimistic and optimistic locking, reservations, and distributed locks.
Contention happens when many requests compete for the same resource at the same time: the last concert ticket, the last item in stock, a driver who can accept only one ride, an account balance. Getting it wrong means double-booking, overselling or lost money — which is why it's one of the most common deep dives.
The core problem
User A reads seat A12: available
User B reads seat A12: available
User A writes seat A12: booked by A
User B writes seat A12: booked by B ← double-bookedEvery solution makes the check and the update atomic, or detects the conflict and rejects one writer.
Solutions within a single database
1. Atomic conditional update
The simplest correct answer — let the database do the check and the write in one statement:
UPDATE seats SET status = 'booked', user_id = :user
WHERE event_id = :event AND seat_id = :seat AND status = 'available';
-- affected rows = 1 → you got it; 0 → someone else did2. Pessimistic locking
Lock the row while you work on it: SELECT … FOR UPDATE inside a transaction. Others wait. Correct and simple, but holding locks for long reduces throughput and risks deadlocks — keep transactions short.
3. Optimistic concurrency control
Read the row with a version number; update only if the version hasn't changed (WHERE version = :readVersion). If it fails, retry. Great when conflicts are rare; poor when many users fight over the same row (lots of retries).
4. Serializable isolation
Let the database detect conflicts and abort a transaction. Correct, but it costs throughput and requires retry logic everywhere.
Reservations: holding a resource for a while
Users need time between choosing a seat and paying. Never hold a database transaction open for minutes. Instead, model a temporary hold:
- Add
status = 'reserved'andreserved_untilcolumns. Reserve with a conditional update that treats expired reservations as available. - Or store holds in Redis with a TTL (
SET hold:A12 user42 NX EX 600), with the database as the source of truth for confirmed bookings. - On payment success, convert the hold into a booking; if the hold expires, the seat becomes available again automatically.
Across services and databases
When the resource and the logic span multiple systems:
- Distributed locks (Redis, ZooKeeper, etcd) — acquire a lock on the resource key with a TTL. Add fencing tokens for correctness: an increasing number that the storage layer checks, so a client that lost its lock (e.g. during a GC pause) can't overwrite newer data.
- Sagas — break a multi-step operation into local transactions with compensating actions (reserve inventory → charge card → if the charge fails, release inventory).
- Two-phase commit — atomic across participants, but blocking and fragile; mention it, then usually prefer sagas.
Extreme contention: one hot item
When thousands of users hit the same resource (a flash sale, a hyped concert), even correct locking turns into a queue at the database.
- Queue the requests — put purchase attempts on a queue partitioned by item and process them sequentially. Serializing requests removes lock contention entirely.
- Virtual waiting room — admit users to the purchase flow in batches.
- Pre-split inventory — divide 1,000 units into 10 buckets of 100 and let requests pick a bucket, reducing per-row contention.
[!INTERVIEW] Match the solution to the contention level "Seat conflicts are rare in general, but extreme for hot events. I'll use a conditional update for correctness everywhere, Redis holds with a 10-minute TTL for the checkout window, and a virtual queue in front of booking for events we flag as high demand."