By the end of this lesson, you will understand:
- What race conditions are and why they're dangerous
- How to use database transactions for atomicity
- Conditional updates to prevent overselling
- How to implement safe stock management
Imagine you have 1 item in stock. Two users try to reserve it at the exact same time:
Timeline:
Time Request A Request B
───────────────────────────────────────────────────────────
0ms Read: availableQty = 3 Read: availableQty = 3
1ms Check: 3 >= 2 ✓ Check: 3 >= 2 ✓
2ms Write: availableQty = 1 Write: availableQty = 1
3ms Return: Success Return: Success
Result: Both requests succeed, but you only had 3 items. Now you've oversold by 1!
- Request A reads the value
- Request B reads the same value (before A writes)
- Both write based on stale data
This is called a race condition - the timing of requests affects correctness.
Instead of:
// ❌ NOT ATOMIC - Two separate operations
const item = db.getItem(itemId);
if (item.availableQty >= requested) {
item.availableQty -= requested; // Another request might sneak in here!
db.update(item);
}Do this:
// ✅ ATOMIC - Single database operation
db.execute(`
UPDATE items
SET availableQty = availableQty - ?
WHERE id = ?
AND availableQty >= ?
`, [requested, itemId, requested]);The WHERE availableQty >= ? condition ensures the update only succeeds if there's enough stock.
Attempt 1: UPDATE items SET availableQty = 2 WHERE id = 'item_1' AND availableQty >= 3
→ Changes 1 row (success)
Attempt 2: UPDATE items SET availableQty = 2 WHERE id = 'item_1' AND availableQty >= 3
→ Changes 0 rows (fails - only 2 left)
export function reserveItem(request: ReserveRequest): ReserveResult {
return transaction(() => {
// Step 1: Check if item exists
const item = db.prepare('SELECT * FROM items WHERE id = ?').get(itemId);
if (!item) return { kind: 'NOT_FOUND' };
// Step 2: Atomically decrement stock
const updated = db.prepare(`
UPDATE items
SET availableQty = availableQty - ?
WHERE id = ? AND availableQty >= ?
`).run(qty, itemId, qty);
if (updated.changes === 0) {
return { kind: 'OUT_OF_STOCK', available: item.availableQty };
}
// Step 3: Create reservation (only if stock update succeeded)
db.prepare(`INSERT INTO reservations ...`).run(...);
return { kind: 'OK', reservation };
});
}Transactions ensure all-or-nothing execution:
transaction(() => {
// All these operations succeed or none do
step1();
step2();
step3();
});If any step fails:
- All changes are rolled back
- Database returns to previous state
- No partial updates occur
UPDATE items
SET availableQty = availableQty - 1, version = version + 1
WHERE id = ? AND version = ?db.transaction(() => {
db.execute('SELECT * FROM items WHERE id = ? FOR UPDATE', [id]);
// No one can modify this row until transaction commits
});UPDATE items
SET availableQty = availableQty - ?
WHERE id = ? AND availableQty >= ?# Terminal 1: Reserve 2 items
curl -X POST http://localhost:3000/api/v1/reserve \
-d '{"userId":"user_1","itemId":"item_1","qty":2}'
# Terminal 2: At the same time, reserve 2 items
curl -X POST http://localhost:3000/api/v1/reserve \
-d '{"userId":"user_2","itemId":"item_1","qty":2}'- First request: Success (reserves 2)
- Second request: Fails (OUT_OF_STOCK)
| File | Purpose |
|---|---|
src/services/reservations.ts |
Atomic business logic |
src/database/index.ts |
Transaction helper |
src/routes/index.ts |
Usage in endpoints |
UPDATE products
SET stock = stock - ?
WHERE id = ? AND stock >= ?UPDATE accounts
SET balance = balance - ?
WHERE id = ? AND balance >= ?UPDATE seats
SET status = 'reserved'
WHERE event_id = ? AND seat_number = ? AND status = 'available'const stock = getItemStock(itemId);
if (stock >= qty) {
reserveItem(itemId, qty); // Race condition!
}const result = db.prepare(`
UPDATE items
SET availableQty = availableQty - ?
WHERE id = ? AND availableQty >= ?
`).run(qty, itemId, qty);
if (result.changes === 0) {
return { error: 'OUT_OF_STOCK' };
}// Bad: Doesn't work across multiple server instances
let lock = false;
if (!lock) {
lock = true;
// ... do work ...
lock = false;
}// Good: Database handles locking correctly
db.transaction(() => {
// ... do work ...
});- Race conditions = timing affects correctness
- Atomic operations = check and update together
- Transactions = all-or-nothing execution
- Conditional updates =
WHEREclause prevents invalid states - Test concurrent requests = verify no overselling
Task: What happens if you remove the AND availableQty >= ? condition?
- Edit
src/services/reservations.ts - Change the UPDATE to:
UPDATE items SET availableQty = availableQty - ? WHERE id = ?
- Run two concurrent reserve requests
- Check
availableQty- it will likely be negative!
Continue to Lesson 4: Idempotency to learn how to handle duplicate requests safely.
💡 Tip: Use transaction() from src/database/index.ts to wrap multi-step operations!