Stock decrement outside a database transaction is a textbook race condition: two simultaneous order requests both read stock = 1, both pass the availability check, both write stock = 0, and one physical item ships twice. CWE-362 (Concurrent Execution Using Shared Resource with Improper Synchronization) names this exact failure. At low traffic the window is small but non-zero; at peak sale traffic (Black Friday, flash sales) it becomes near-certain. ISO 25010:2011 fault-tolerance requires the system to handle concurrent requests correctly — a separate findUnique + update pattern fails this at the architecture level, not the implementation level.
Critical because a race condition in stock decrement causes confirmed orders for items that don't exist, resulting in fulfillment failures and customer refund obligations that grow proportionally with traffic.
Wrap the stock check and decrement in a Prisma $transaction in src/app/api/orders/route.ts so both operations execute atomically.
const order = await prisma.$transaction(async (tx) => {
const product = await tx.product.findUnique({ where: { id: productId } })
if (!product || product.stock < quantityOrdered) {
throw new Error('Insufficient stock')
}
await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantityOrdered } },
})
return tx.order.create({ data: { productId, quantity: quantityOrdered } })
})
For high-concurrency scenarios, consider a Postgres row-level lock (SELECT FOR UPDATE) or an optimistic concurrency check (WHERE stock >= quantityOrdered in the update predicate) to further tighten the window.
ID: ecommerce-catalog.inventory.stock-decrement-on-order
Severity: critical
What to look for: Count all order placement or checkout paths in the codebase (src/app/api/orders/route.ts, src/app/api/checkout/route.ts, server actions). For each path, enumerate the stock decrement logic: is stock decremented in a database transaction ($transaction, BEGIN/COMMIT), or in separate application-level steps? Check for race condition patterns — two simultaneous requests reading the same stock value.
Before evaluating: Extract and quote the stock decrement code from the order placement path. Show the exact transaction boundary (or lack thereof).
Pass criteria: When an order is placed, stock is decremented exactly once per item using a database transaction (Prisma $transaction, SQL BEGIN/COMMIT, or atomic update) to prevent race conditions. At least 1 order placement path must exist. Report: "X of Y order paths use transactional stock decrement."
Fail criteria: Stock is not decremented on order placement, or stock decrement happens outside a transaction (separate read-then-write that is vulnerable to race conditions), or stock could be decremented twice for a single order.
Skip (N/A) when: Never — stock management is critical for e-commerce.
Cross-reference: For transaction patterns and concurrency control, the Database Design & Operations audit covers transaction isolation levels and locking strategies.
Cross-reference: For order processing reliability, the Error Resilience audit covers retry logic and idempotency patterns.
Cross-reference: For checkout flow security, the API Security audit covers request atomicity and double-submit prevention.
Detail on fail: "Stock decrement in src/app/api/orders/route.ts uses separate findUnique + update without $transaction. Two simultaneous orders could both read stock=10 and both decrement to 9" or "No stock decrement found in any checkout path"
Remediation: Decrement stock within a database transaction in src/app/api/orders/route.ts:
const result = await prisma.$transaction(async (tx) => {
const product = await tx.product.findUnique({
where: { id: productId },
})
if (product.stock < quantityOrdered) {
throw new Error('Insufficient stock')
}
const updated = await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantityOrdered } },
})
return updated
})