Pessimistic vs Optimistic Locking
Choose between locking rows up front and detecting conflicts with versions—trade throughput, latency, and user experience under contention.
Concurrent updates to the same row need a policy. Pessimistic locking assumes conflict is likely and serialises writers with locks. Optimistic concurrency control (OCC) assumes conflict is rare, lets transactions proceed, and aborts when a version check fails. Neither is universally better; contention rate and conflict cost decide.
Pessimistic locking
A transaction acquires a lock before reading or updating protected data. Others wait (or fail quickly) until the lock is released at commit/rollback.
BEGIN;
SELECT balance FROM accounts WHERE id = @id FOR UPDATE;
-- application logic using balance
UPDATE accounts SET balance = @newBalance WHERE id = @id;
COMMIT;| Strength | Weakness |
|---|---|
| Simple mental model under high contention | Lock waits increase latency |
| Prevents wasted work on doomed updates | Deadlocks possible |
| Good for short critical sections | Long locks (especially with remote I/O) kill throughput |
- Pessimistic lock
A database lock taken early so other transactions cannot modify the row until the holder commits or rolls back.
Optimistic concurrency
Each row carries a version (integer, timestamp, or rowversion). Readers note the version; writers include it in the update predicate. If zero rows update, someone else won—retry or surface a conflict.
UPDATE accounts
SET balance = @newBalance, version = version + 1
WHERE id = @id AND version = @expectedVersion;
-- if rowcount = 0 → conflictpublic async Task WithdrawAsync(AccountId id, Money amount, CancellationToken ct)
{
for (var attempt = 0; attempt < 5; attempt++)
{
var account = await _store.GetAsync(id, ct);
account.Withdraw(amount); // domain invariant
var updated = await _store.TrySaveAsync(account, ct); // WHERE version = ...
if (updated) return;
// conflict — reload and retry
}
throw new ConcurrencyException(id);
}| Strength | Weakness |
|---|---|
| No lock waits on the happy path | High contention → many retries |
| Scales well for rare conflicts | Users may see “someone else edited this” |
| Fits disconnected/UIs and HTTP APIs | Must handle idempotency on retry |
- Optimistic concurrency control
Allow concurrent work without locks; detect write-write conflicts at commit time using versions or compare-and-swap predicates.
Side-by-side
BEGIN;SELECT * FROM documentsWHERE id = @idFOR UPDATE;UPDATE documentsSET body = @bodyWHERE id = @id;COMMIT;UPDATE documentsSET body = @body, version = version + 1WHERE id = @idAND version = @version;-- 0 rows → reload / retry / conflictChoosing a strategy
| Situation | Prefer |
|---|---|
| Hot row, short critical section (inventory decrement) | Pessimistic or atomic single-statement update |
| User-edited documents, low simultaneous editors | Optimistic |
| HTTP APIs with retries | Optimistic + idempotency keys |
| Cross-row invariants needing serialisation | Pessimistic or carefully designed atomic SQL |
| Distributed shards without shared locks | Optimistic / compare-and-swap locally per shard |
Often the best “lock” is neither: a single SQL statement that encodes the invariant (UPDATE ... WHERE quantity >= @qty), which is atomic without an explicit select-for-update round trip.
Isolation levels and surprises
Pessimistic locks interact with isolation levels (READ COMMITTED vs REPEATABLE READ vs SERIALIZABLE). OCC often pairs with detecting lost updates even under weaker isolation by using versions. Do not assume “we use transactions” alone prevents lost updates—lost update is classic under read-modify-write without protection.
Fit in application architecture
In hexagonal architecture, version fields live on persistence models or aggregates; adapters translate conflicts into domain results. Under scaling writes, hot keys may need queues in addition to locking so the database is not the only scheduler. With sharding, locks do not span shards—design aggregates to stay on one shard.
Practical guidelines
- Prefer atomic SQL for simple counters and balances
- Use OCC for collaborative edits and APIs
- Use pessimistic locks for brief, high-conflict server-side sections
- Cap retries with backoff; surface conflicts to users when appropriate
- Measure conflict rates—OCC that retries endlessly is a design smell