LESSON 18.4
Optimistic concurrency and conditional writes
Version numbers and compare-and-set, ETags and conditional requests, row locks with SELECT FOR UPDATE, and choosing between optimistic and pessimistic control for contended data.
6 min read
Most coordination doesn't need a lock service at all. When the data lives in one store, the store itself can resolve races, either by refusing a write that was based on stale data or by making competing writers wait their turn.
The lost update
- Admin A reads a product (stock 10, price 20).
- Admin B reads the same product.
- A changes the price to 25 and saves the whole record.
- B changes stock to 8 and saves the whole record, with the old price 20.
A's change is gone, and nobody got an error. Read-modify-write without coordination always risks this.
Optimistic concurrency
Give each record a version. Every update includes the version it was based on, and the database applies it only if that's still current:
UPDATE products SET price = 25, version = 8 WHERE id = 42 AND version = 7
If another write got there first, zero rows are updated, and the application returns a conflict. The client reloads, reapplies its change or asks the user to merge. No locks are held while the user thinks, so it suits web forms and APIs well.
The same idea goes by different names:
- SQL
- WHERE version = ? on UPDATE
- HTTP
- ETag + If-Match, 412 on mismatch
- DynamoDB
- ConditionExpression on PutItem or UpdateItem
- Redis
- WATCH + MULTI/EXEC, or a Lua script
- Kubernetes, etcd
- resourceVersion or mod revision checks
Atomic updates avoid the race entirely
Many read-modify-write cycles don't need to read at all. UPDATE seats SET held_by = 'u1' WHERE id = 9 AND held_by IS NULL both checks and claims in one statement. UPDATE stock SET count = count - 1 WHERE id = 3 AND count > 0 decrements without ever going negative. Pushing the condition into a single write is the simplest concurrency control there is.
Pessimistic locking
SELECT ... FOR UPDATE locks the rows it reads until the transaction ends. Other writers wait instead of failing.
Optimistic (versions, conditions)
- No waiting, no locks held
- Works across requests and user think time
- Retries under contention
Pick it when: Conflicts are rare: profiles, settings, documents, most CRUD.
Pessimistic (row locks)
- No wasted retries
- Simple to reason about inside one transaction
- Waiting, deadlocks
- Must never span user think time
Pick it when: Hot rows with frequent conflicts: inventory, seat holds, account balances.
Keep locked transactions short, lock rows in a consistent order to avoid deadlocks, and set lock timeouts. SKIP LOCKED is a useful variant for work queues: each worker takes rows nobody else has locked.
When the store can't help
If the race spans systems (write a file, then call an API), there's no single place to compare-and-set. That's when you need leases with fencing tokens from the earlier lesson, or a redesign so one system owns the decision.
Try it
Event Ticketing (Ticketmaster) HLD
Prevent double-booking under a rush of buyers.