The classic race
"Read inventory; if > 0, decrement and confirm sale." Two requests run this in parallel. Both read inventory = 1. Both decide it's safe. Both decrement. Inventory is now -1, you sold something you don't have. The bug isn't in any single line — it's in the gap between the read and the write.
Three fixes, each appropriate sometimes
- Atomic UPDATE — push the check and decrement into one statement:
UPDATE products SET inventory = inventory - 1 WHERE id = ? AND inventory > 0. If 0 rows updated, the sale failed. - SELECT FOR UPDATE — lock the row when you read it; do the conditional logic in app code; UPDATE; COMMIT.
- SERIALIZABLE isolation — let PostgreSQL detect the conflict and ask you to retry.
The most common one
Atomic UPDATE is usually the cleanest answer. It's one round trip, no explicit lock, no retry. Reach for SELECT FOR UPDATE when the conditional logic is too complex to fit into a single SQL statement.