advanced 3 min answer

A voucher-redemption feature creates a single hot row that thousands of buyers hit in the same second. Compare row-level locking, optimistic concurrency, bucketed counters and an in-memory reservation service.

hot-rowcontentionoptimistic-concurrencyreservationflash-sale
Show the full answer Hide the answer

Why the row is the problem

Every redemption must check and decrement one counter, so every transaction serialises on the same row. Throughput is bounded by the time to acquire the lock, update, and commit — perhaps a millisecond — giving a ceiling around a thousand per second regardless of database size or replica count. Adding hardware changes nothing, which is the defining property of a hot row.

Row-level locking (SELECT ... FOR UPDATE)

Correct and simple. Transactions queue on the lock; the count is exact; no overselling.

Fails at scale: the queue grows, lock wait time rises, connections are held, and the connection pool exhausts — at which point the contention on one row has taken down the whole service. The blast radius extends far beyond the feature.

Acceptable for hundreds per second; not for thousands.

Optimistic concurrency

Read the value and version, compute, write conditionally on the version being unchanged; retry on conflict.

Advantage: no locks held, so no connection exhaustion from waiting.

Fails worse under high contention: the conflict rate rises with concurrency, so most attempts fail and retry, and the retries add load. Optimistic concurrency is optimised for rare conflicts and this scenario is the opposite — it is often worse than locking here, which surprises people.

Bucketed counters

Split the counter into N rows, each holding a share. A redemption picks a bucket at random and decrements it. The total is the sum.

Advantage: contention divided by N. 100 buckets gives roughly 100× the throughput, with the same database and the same guarantees.

Costs: reads must sum all buckets, which is fine at this scale. Buckets deplete unevenly, so some are empty while others have stock — requiring a fallback that tries another bucket, and a rebalancing step. The "sold out" determination becomes non-trivial, since a client may see one empty bucket while inventory remains.

This is the best answer that stays within the database, and it is usually sufficient.

In-memory reservation service

A single service owns the counter in memory, serialises decisions on a single thread, and persists asynchronously.

Advantage: hundreds of thousands of decisions per second, exact counting, no database contention at all. This is how genuine flash sales work.

Costs, and they are real: a durability window — decisions acknowledged but not yet persisted are lost if the process dies, so recovery must reconcile against the persisted log and the business must accept a small error boundary. It is a stateful single-owner service, needing failover with careful ownership transfer to avoid two instances believing they own the counter. And it is a substantial piece of engineering.

Choosing

Hundreds/sec → locking. Thousands/sec → bucketed counters. Tens of thousands/sec and the business depends on it → reservation service.

Do not build the reservation service in anticipation. Build buckets, measure, and build the service when the measurement demands it — the durability and failover complexity is only worth carrying when the throughput genuinely requires it.

The question that must be asked first

"What happens if we oversell by 0.1%?" If the answer is "we apologise and refund," the entire problem becomes much easier and an approximate fast path is acceptable. If the answer is "we are in breach of a legal commitment," exactness is a hard requirement and the design must pay for it. Engineers frequently assume the second without asking, and buy expensive correctness the business did not need.