The seat locker

Taylor Swift drops ticket sales on Friday at 9 AM. 50,000 seats. 2 million fans. Every seat has to go to exactly one buyer, no duplicates, no oversells. Response time has to stay under 500ms or users think the site is broken and hammer it harder.

The race

Two fans hit Reserve Seat 42 at the same millisecond. Without locking, both succeed, both think they own it, and one of them gets refunded with an apology email.

solution-1-pessimistic/reserve.ts
typescript
await db.transaction(async (tx) => {
  const rows = await tx.execute(
    sql`SELECT id, status FROM seats WHERE id = ${seatId} FOR UPDATE`,
  );

  if (rows[0].status !== 'available') {
    throw new Error('Seat already taken');
  }

  await tx.execute(
    sql`UPDATE seats SET status = 'reserved', owner_id = ${userId} WHERE id = ${seatId}`,
  );
});

SELECT FOR UPDATE takes a row lock. The second transaction waits until the first finishes.

solution-2-optimistic/reserve.ts
typescript
const { version, status } = await db.seats.findOne({ id: seatId });
if (status !== 'available') throw new Error('Seat already taken');

const result = await db.execute(
  sql`UPDATE seats
      SET status = 'reserved', owner_id = ${userId}, version = version + 1
      WHERE id = ${seatId} AND version = ${version}`,
);

if (result.rowCount === 0) {
  throw new Error('Seat was taken by another request');
}

Compare version number on the update. If the row changed since you read it, the update affects zero rows and you retry.

Pessimistic is simpler to reason about and always correct, but it blocks other transactions on the same row. Optimistic is non-blocking and scales further, but fails fast under high contention and the caller has to retry. For a 50K-seat venue, the contention is high but localized. Pessimistic is often the right call. For a 50M-row catalog with occasional writes, optimistic wins by a mile.

Quiz: Quiz

Loading practice…