The race
Both requests run SELECT stock FROM items WHERE id=42 and read 1. Neither has written yet, so neither sees the other's intent. Each concludes "stock ≥ 1, sell it" and issues UPDATE items SET stock = stock - 1. Result: stock = -1, two customers, one item. The check and the write are not atomic — another transaction interleaves between them.
Fix 1: row-level lock (SELECT ... FOR UPDATE)
BEGIN;
SELECT stock FROM items WHERE id = 42 FOR UPDATE; -- locks this row only
-- app checks stock >= 1, else ROLLBACK
UPDATE items SET stock = stock - 1 WHERE id = 42;
INSERT INTO orders (item_id, qty) VALUES (42, 1);
COMMIT;
The second transaction blocks on FOR UPDATE until the first commits, then re-reads stock = 0 and aborts.
Fix 2: atomic conditional update (no explicit lock)
UPDATE items
SET stock = stock - 1
WHERE id = 42 AND stock >= 1;
If rowcount = 0, the sale failed — roll back. Postgres locks the single row implicitly during the update.
Belt-and-braces: add CHECK (stock >= 0) on the column.