Adios
BlogNext.js Ecommerce

Next.js Ecommerce

How to Manage Inventory and Cache Invalidation in a Next.js Store

Prevent overselling with transactional inventory, reservations, idempotent webhooks, Next.js cache tags, targeted invalidation, and concurrency tests.

Adios teamUpdated July 17, 20268 min read

Inventory is a transaction, not a cache value. The cache may tell shoppers what was recently available; only the source-of-truth write may promise the last unit.

Define available, reserved, and sold

Trail Supply has two medium blue Alpine Shells. onHand is the physical count, reserved is temporarily claimed by active checkout attempts, and sold belongs to confirmed orders. Available is derived from those durable states rather than stored as an unrelated number that can drift.

Define when a reservation begins, how long it lasts, and which event commits or releases it. Reserving at add-to-cart is usually too early; reserving at Checkout may fit limited inventory; reserving at payment requires a policy for simultaneous buyers.

Make the stock change atomic

A read-then-write sequence allows two requests to observe the same last unit. Use a database transaction, row lock, conditional update, or another atomic primitive supported by the data store. The write succeeds only when enough unreserved stock remains.

Enforce nonnegative quantities with database constraints where possible. Record the reservation, variant, quantity, owner or pending order, creation time, expiry, and status in the same transaction that changes availability.

const reserved = await db.inventory.updateMany({
  where: {
    variantId,
    available: { gte: quantity },
  },
  data: {
    available: { decrement: quantity },
    reserved: { increment: quantity },
  },
});

if (reserved.count !== 1) {
  return { error: "Insufficient stock" };
}

Expire reservations deliberately

Store an expiry and run a scheduled recovery job that releases active reservations whose Checkout Session can no longer complete. Make release idempotent: changing an already committed or released reservation must do nothing.

Late events need a policy. If payment completes after local expiry, reconcile against provider and fulfillment state rather than blindly recreating stock or canceling a paid order. Some stores allow backorder; others refund. Encode the product decision.

Cache catalog reads, not authority

With Cache Components enabled, cache product and category presentation using use cache, a suitable cacheLife, and tags for the product, variant, and affected categories. The displayed availability can be slightly stale only if checkout performs an authoritative validation.

Keep cart and inventory mutation code outside shared public caches. User-specific carts need separate request or private cache behavior. Never place a session-bound inventory promise in a key that another shopper can receive.

export async function getProduct(slug: string) {
  "use cache";
  cacheLife("minutes");
  cacheTag("product:" + slug);
  return loadProductView(slug);
}

Invalidate the smallest correct set

After a stock or price mutation, invalidate the product and each category page that visibly contains the changed value. updateTag gives a Server Action read-your-writes behavior; revalidateTag with an appropriate profile suits content that may serve stale data while refreshing.

Do not clear every cached store page after one medium blue jacket changes. Broad invalidation increases load and makes cache behavior harder to reason about. Centralize tag construction so publishers, webhooks, and admin actions name the same entries.

Reconcile warehouse and payment events

Record external event IDs before applying stock changes. A warehouse adjustment, cancellation, refund, and payment completion can repeat or arrive in an unexpected order. Normalize each into a local inventory transition and reject transitions that do not follow from current state.

Keep an append-only movement ledger or sufficient audit history to explain the total. A current quantity without its reservation and order history is difficult to repair after a provider or deployment failure.

Load-test contention and recovery

Send many concurrent reservation requests against the final two units and assert that no more than two succeed. Interrupt a worker, repeat a payment event, expire a reservation, deliver a late completion, and rebuild the cached product view. Check both database invariants and customer-visible results.

Run those tests against the production build in an Adios preview. Runtime logs expose contention and webhook errors, while the database and secret dependencies remain explicit in the manifest. Promote after the health route and inventory smoke tests pass.

  All articles