# Stock decrements on order placement without double-counting

- **Pattern:** `ab-001126` (`ecommerce-catalog.inventory.stock-decrement-on-order`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001126
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001126)

## Why it matters

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.

## Severity rationale

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.

## Remediation

Wrap the stock check and decrement in a Prisma `$transaction` in `src/app/api/orders/route.ts` so both operations execute atomically.

```ts
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.

## Detection

- **ID:** `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`:

  ```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
  })
  ```

## External references

- cwe CWE-362
- iso-25010 fault-tolerance

Taxons: data-integrity, error-resilience

HTML version: https://auditbuffet.com/patterns/ab-001126
