A bid becomes the price
Illustrative exampleNumbers here are illustrative: round figures to reason with, not measurements.
The bid service first reserves the bid’s key, then applies the bid with one conditional update on the auction row: UPDATE auctions SET price = …,, inserting the bid and an outbox row in the same transaction. If a row changed, the bidder leads; if not, the service re-reads and answers outbid, too low or ended.
The trap is reading the price, checking it in code and writing it back, even inside a transaction. At PostgreSQL’s default Read Committed level, a second update waits for the first and then re-checks its WHERE clause against the updated row (PostgreSQL docs (opens in a new tab)), which is what makes the conditional update safe and what a check in code skips. Bids arriving together are applied one after another; a lower bid can lead for a moment, but it never replaces a higher one.
- The app sends the bid with its key.
- The bid service reserves the key, then runs one conditional update on the auction row.
- The bid record and an outbox row commit with it.
- The bidder hears leading, outbid, too low or ended.
Diagram as text
Components: Bid of $110 (Client); Bid of $105 (Client); Bid service (Service); Auction row, price $100 (SQL DB); Bid and outbox rows (SQL DB). Connections: Bid of $110 → Bid service (key); Bid of $105 → Bid service (key); Bid service → Auction row, price $100 (one update each); Auction row, price $100 → Bid and outbox rows (same transaction).
Read:Learn: Consistency trade-offsLearn: PostgreSQL
Check yourself
Bids of $110 and $105 hit a $100 auction together, and the $105 one commits first. What does the $110 bidder see?
Show answerHide answer
Their update waits, re-checks against $105, still beats it by the increment, and leads. The $105 bidder led for a moment and is then told they were outbid.