For multi-step flows with meaningful capacity constraints, optimistic locking (version fields) surfaces conflicts only at the final commit — after the user has filled the booking form and possibly entered payment details. CWE-362 (race condition) describes the hazard: concurrent requests succeed their availability checks, race to commit, and one fails at the very last step. Pessimistic locking with FOR UPDATE queues concurrent requests sequentially, so each one sees the committed state of the previous one and the user receives immediate feedback before investing time in the flow.
Low because UNIQUE constraints provide the primary double-booking defense; pessimistic locking reduces late-stage UX failures but is not the sole concurrency control.
Acquire a row-level lock on the slot before inserting, inside a transaction. Apply this in your booking service (e.g., src/lib/booking-service.ts).
-- Raw SQL inside a transaction
BEGIN;
SELECT id, booked_count, max_capacity
FROM slots
WHERE id = $1
FOR UPDATE; -- locks the row for this transaction
-- check capacity, insert booking
COMMIT;
With Prisma, use $queryRaw with FOR UPDATE inside a $transaction callback.
ID: booking-flow-lifecycle.double-booking.pessimistic-locking
Label: Pessimistic locking is used
Severity: low
What to look for: Count all locking patterns in booking-related code: FOR UPDATE (raw SQL), Prisma's $queryRaw with FOR UPDATE, Sequelize's lock: Transaction.LOCK.UPDATE, Drizzle's .for('update'), TypeORM's setLock('pessimistic_write'), or Redis-based distributed locks (SET ... NX EX). Also identify if optimistic locking (version column checks) is used instead. Enumerate: "X locking mechanisms found across Y booking operations."
Pass criteria: At least 1 pessimistic locking mechanism is used in booking creation or modification flows. The code locks the slot/resource row for updates during the booking process to queue concurrent requests sequentially. Report: "X pessimistic locks found: [FOR UPDATE in file:line | Redis NX lock in file:line]."
Fail criteria: Optimistic locking (version fields) is used where user experience would suffer (user fills entire form then fails at the very end), or no locking strategy at all in any booking operation.
Skip (N/A) when: The system uses unique constraints alone for a simple 1:1 booking with no multi-step flow, and no capacity-based slots exist.
Detail on fail: "No row-level locking (FOR UPDATE) detected. Under high traffic, many concurrent booking attempts would fail with 'slot unavailable' errors."
Cross-reference: The concurrent-request-handling check in this category covers UNIQUE constraints as the primary defense; pessimistic locking is a complementary layer.
Cross-reference: The time-slot-lock check in Booking Creation covers temporary holds for the user-facing flow; this check covers database-level row locking.
Cross-reference: The slot-availability-recheck check in this category verifies the recheck happens inside the locked transaction.
Remediation: Implement row-level locking in your transaction block (e.g., in src/lib/booking-service.ts or your booking API handler).
BEGIN;
SELECT * FROM slots WHERE id = $1 FOR UPDATE;
-- Verify capacity, insert booking
COMMIT;