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-booked

Every 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 did

2. 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' and reserved_until columns. 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."