Booking creation typically writes to multiple tables: the booking record, the slot's capacity counter, a payment reference, and possibly an audit log. Sequential await db.*.create/update calls outside a transaction are not atomic — if any step fails, all previous writes remain committed. CWE-362 and CWE-841 both apply: partial writes leave orphaned booking records with no slot decrement, or a slot marked unavailable with no corresponding booking. These orphaned records accumulate silently and are only discovered when a customer reports a missing booking or a slot appears permanently blocked.
Critical because non-atomic multi-table writes produce orphaned records on partial failure, causing silent data corruption that is difficult to detect and expensive to remediate.
Wrap every multi-table mutation in a single transaction in your booking service (e.g., src/lib/booking-service.ts). All writes succeed together or none does.
await db.$transaction([
db.booking.create({ data: { ...bookingData, status: 'CONFIRMED' } }),
db.slot.update({
where: { id: slotId },
data: { bookedCount: { increment: 1 } }
}),
]);
ID: booking-flow-lifecycle.double-booking.transaction-atomicity
Label: Transaction atomicity enforced
Severity: critical
What to look for: Count all database tables modified during a single booking creation flow (booking record, slot status, payment record, audit log, notification log, etc.). Then verify that all these mutations happen in a single transaction block. Look for db.$transaction, sequelize.transaction(), knex.transaction(), BEGIN...COMMIT in raw SQL, or equivalent. Count: "X tables modified in booking creation; Y of X are inside a transaction."
Pass criteria: All related database mutations (at least 2 tables) are wrapped in a single transaction (Prisma $transaction, Sequelize transaction(), SQL BEGIN...COMMIT). If any step fails, all preceding steps are rolled back. Report: "X of Y table mutations wrapped in transaction in [file:line]."
Fail criteria: Mutations happen in separate calls outside a transaction. If step 2 fails, step 1 remains (data inconsistency, orphaned records). Do NOT pass when multiple await db.*.create/update calls appear sequentially without a wrapping transaction — even if they "usually work," they are not atomic.
Skip (N/A) when: Only a single database table is modified during booking creation (no slot status update, no related records).
Detail on fail: "Booking creation and Slot status update happen in separate Prisma calls. If Slot update fails, an orphaned PENDING booking remains."
Cross-reference: The concurrent-request-handling check in this category verifies the UNIQUE constraint that prevents duplicates within this transaction.
Cross-reference: The cancellation-state-machine check in Lifecycle Management verifies cancellation uses similar transactional writes.
Cross-reference: The rescheduling-logic check in Lifecycle Management verifies atomic slot swaps.
Remediation: Wrap all related mutations in a single transaction in your booking service (e.g., src/lib/booking-service.ts).
await db.$transaction([
db.booking.create({ data: {...} }),
db.slot.update({ where: { id: slotId }, data: { available: false } }),
]);