Pattern · Consistency
Dealing with contention
Many requests competing for the same thing, such as the last seat, one inventory count or a shared counter. Locks, optimistic concurrency, atomic operations, reservations and queues, and when to use each.
11 min read
Contention happens when many requests want to change the same piece of data at once: the last concert ticket, an item in a flash sale, an auction's highest bid, a shared counter. Get it wrong and you oversell or double-book; get it heavy-handed and everything queues behind one lock.
The problem
The classic race is read, check, write:
- Request A reads "1 seat left".
- Request B reads "1 seat left".
- Both decide the seat is available and both write a booking.
Any fix makes the check and the write atomic, or serializes the requests.
Solutions within one database
Atomic conditional update
- One statement, no explicit lock
- Fast
- Only for simple conditions
Pick it when: UPDATE stock SET n = n - 1 WHERE id = 42 AND n > 0, then check rows affected.
Pessimistic lock (SELECT … FOR UPDATE)
- Simple reasoning
- Handles complex checks
- Waiters queue on the row
- Deadlocks if lock order varies
Pick it when: Short transactions with real contention on a few rows.
Optimistic concurrency (version column)
- No locks held
- Great when conflicts are rare
- Retries pile up under heavy contention
Pick it when: Edits to documents and profiles; occasional conflicts.
Unique constraint
- The database guarantees it
- Only for uniqueness rules
Pick it when: One booking per seat: UNIQUE (event_id, seat_id).
Let the database enforce invariants wherever you can. A unique index or a conditional update can't be bypassed by a buggy code path.
Reservations with expiry
For checkouts that take minutes, don't hold a database lock while the user types their card number. Instead:
- Reserve: atomically mark the seat HELD with an expiry, say 10 minutes, and the holder's id.
- Confirm: on successful payment, change HELD to SOLD if the hold is still yours.
- Expire: a sweeper (or the next reader) treats holds past their expiry as free.
This is how ticketing and hotel booking systems avoid both overselling and long-held locks.
Extreme contention
When thousands of requests hit one item per second (a flash sale, a hyped drop), even row locks become the bottleneck.
- Queue or waiting room: admit users at a controlled rate; everyone else waits with a position.
- Atomic counters in Redis:
DECRstock in memory (or a Lua script that checks and decrements), then write the order durably. Reconcile against the database. - Split the inventory: divide 10,000 units into 10 buckets on different keys, so requests spread across them.
- Serialize per item: route all requests for one item to one partition or actor that processes them in order.
Across services
When the contested resource lives in one service and the decision spans others, use a reservation step inside a saga: reserve, do the other steps, then confirm or release. Distributed locks across services are a last resort; if you use one, protect the resource with fencing tokens.
Try it
Event Ticketing (Ticketmaster) HLD
Sell seats for a huge on-sale without double-booking.