Designing Data-Intensive Applications
Case 4

Preventing Double Booking

Use database constraints, compare-and-swap, or dedicated inventory services — never 'check then insert' without locking.

Two users booking the last seat is a race. The fix is atomic decrement: one transaction wins, the other gets a clear 'sold out' error — not two confirmations.

typescript — Atomic inventory decrement
// Atomic inventory — prevent double booking
await prisma.$transaction(async (tx) => {
  const slot = await tx.inventory.findUnique({
    where: { listingId_date: { listingId, date } },
  });
  if (!slot || slot.available < 1) throw new Error("SOLD_OUT");
  await tx.inventory.update({
    where: { listingId_date: { listingId, date }, version: slot.version },
    data: { available: { decrement: 1 }, version: { increment: 1 } },
  });
});
Diagram
In practice

OpenTable and Ticketmaster use inventory partitions per venue/show time. Airbnb shards booking writes by listing ID. All share: single writer per inventory cell at commit time.

Hold vs confirm

A hold reserves inventory temporarily; confirm converts hold to sale after payment. Never confirm without checking hold ownership.

Key Takeaways
  • Pessimistic locking (SELECT FOR UPDATE) on inventory rows is simple and strong.
  • Optimistic concurrency with version columns works when conflicts are rare.
  • Distributed locks (Redis Redlock) only if DB row is unavailable — know the risks.
  • Hold tokens expire via TTL job to release abandoned carts.
  • Idempotent booking API keys prevent duplicate charges on retry.
  • Outbox pattern publishes booking events after commit.
lockingserializableidempotencyholdoutbox