Skip to main content

Checkout Inventory Flow

The inventory flow during checkout ensures that products are reserved when checkout starts and committed when payment is confirmed. This prevents overselling and ensures inventory consistency.

Inventory Flow Overview

Flow States

Inventory States

  • AVAILABLE: Product available for purchase
  • RESERVED: Inventory reserved during checkout
  • COMMITTED: Inventory committed after payment (deducted from stock)

Reservation Process

When Reservations Happen

Inventory is reserved when:

  1. Checkout Started: Cart is locked, inventory reserved
  2. Items Added to Cart: Inventory reserved immediately (optional, configurable)

Reservation Flow

Reservation Implementation

async reserveInventory(
cartId: string,
variantId: string,
quantity: number,
): Promise<void> {
// Check available inventory
const available = await this.getAvailableInventory(variantId);
const reserved = await this.getReservedInventory(variantId);
const actuallyAvailable = available - reserved;

if (actuallyAvailable < quantity) {
throw new BadRequestException(
`Insufficient inventory. Available: ${actuallyAvailable}`
);
}

// Create reservation key
const reservationKey = `inventory:reservation:${cartId}:${variantId}`;

// Increment reserved count atomically
await this.client.incrby(`inventory:reserved:${variantId}`, quantity);

// Store individual reservation
await this.client.set(
reservationKey,
quantity.toString(),
"EX",
RESERVATION_TTL, // 30 minutes
);
}

Reservation Storage

Redis Key Patterns

// Individual reservation per cart item
inventory:reservation:{cartId}:{variantId} → quantity

// Aggregated reserved count per variant
inventory:reserved:{variantId} → totalReserved

// Available inventory (from database)
inventory:available:{variantId} → availableCount

Reservation TTL

  • Default TTL: 30 minutes
  • Auto-Release: Reservations expire automatically
  • Refresh: TTL refreshed on cart activity

Reservation Refresh

// Refresh reservation TTL on cart activity
async refreshReservationTTL(cartId: string, variantId: string): Promise<void> {
const reservationKey = `inventory:reservation:${cartId}:${variantId}`;
await this.client.expire(reservationKey, RESERVATION_TTL);
}

Inventory Availability Calculation

Available Inventory Formula

availableInventory = totalInventory - reservedInventory - committedInventory

Real-time Availability

async getAvailableInventory(variantId: string): Promise<number> {
// Get total inventory from database
const total = await this.getTotalInventory(variantId);

// Get reserved inventory from Redis
const reserved = await this.getReservedInventory(variantId);

// Calculate available
return Math.max(0, total - reserved);
}

Reserved Inventory Calculation

async getReservedInventory(variantId: string): Promise<number> {
const key = `inventory:reserved:${variantId}`;
const reserved = await this.client.get(key);
return reserved ? parseInt(reserved, 10) : 0;
}

Bundle Inventory Reservations

Bundle Reservation Flow

// Flatten bundle selections into variant quantities
const variantQuantities = flattenBundleSelections(
selections,
bundleQuantity,
);

// Reserve inventory for each variant
for (const vq of variantQuantities) {
await this.inventoryStore.reserveInventory(
cartId,
vq.variantId,
vq.quantity,
);

// Refresh TTL
await this.inventoryStore.refreshReservationTTL(cartId, vq.variantId);
}

Example Bundle Reservation

For a bundle with:

  • Set 1: Variant A (quantity: 1)
  • Set 2: Variant B (quantity: 2)
  • Bundle Quantity: 2

Reservations:

  • Variant A: 2 units reserved
  • Variant B: 4 units reserved

Inventory Commit

When Inventory is Committed

Inventory is committed when:

  1. Payment Confirmed: Payment webhook received
  2. Order Created: Order creation successful
  3. Inventory Deducted: Stock reduced permanently

Commit Flow

Commit Implementation

async commitInventory(orderId: string, cartItems: CartItem[]): Promise<void> {
// Release all cart reservations
await this.inventoryStore.releaseCartReservations(cart.id);

// Commit inventory for variant items
for (const item of variantItems) {
await this.inventoryStore.incrementInventory(
item.productVariantId,
-item.quantity, // Negative to decrement
);
}

// Commit inventory for bundle items
for (const bundleItem of bundleItems) {
const variantQuantities = flattenBundleSelections(
bundleItem.metadata.selections,
bundleItem.quantity,
);

for (const vq of variantQuantities) {
await this.inventoryStore.incrementInventory(
vq.variantId,
-vq.quantity,
);
}
}
}

Inventory Release

When Inventory is Released

Inventory is released when:

  1. Payment Failed: Payment declined or failed
  2. Checkout Cancelled: Customer cancels checkout
  3. Reservation Expired: TTL expired
  4. Cart Cleared: Cart items removed

Release Flow

async releaseInventory(
cartId: string,
variantId: string,
quantity: number,
): Promise<void> {
// Decrement reserved count
await this.client.decrby(
`inventory:reserved:${variantId}`,
quantity,
);

// Delete individual reservation
const reservationKey = `inventory:reservation:${cartId}:${variantId}`;
await this.client.del(reservationKey);
}

Release All Cart Reservations

async releaseCartReservations(cartId: string): Promise<void> {
// Find all reservation keys for this cart
const pattern = `inventory:reservation:${cartId}:*`;
const keys = await this.client.keys(pattern);

// Release each reservation
for (const key of keys) {
const quantity = parseInt(await this.client.get(key) || "0", 10);
const variantId = extractVariantId(key);

// Decrement reserved count
await this.client.decrby(`inventory:reserved:${variantId}`, quantity);

// Delete reservation key
await this.client.del(key);
}
}

Payment Failure Handling

Inventory Release on Failure

async handlePaymentFailure(checkoutSessionId: string): Promise<void> {
const session = await this.checkoutStore.getSession(checkoutSessionId);

// Release all inventory reservations
await this.inventoryStore.releaseCartReservations(session.cartId);

// Unlock cart
await this.checkoutStore.releaseCheckoutLock(session.cartId);

// Update checkout state
await this.checkoutStore.transitionState(
checkoutSessionId,
CheckoutState.FAILED,
);
}

Inventory Reconciliation

Reservation Reconciliation

Periodically reconcile reservations:

async reconcileReservations(variantId: string): Promise<void> {
// Get aggregated reserved count
const aggregatedReserved = await this.getReservedInventory(variantId);

// Get all individual reservations
const reservations = await this.getAllReservations(variantId);
const totalFromReservations = sumReservations(reservations);

// Fix inconsistencies
if (aggregatedReserved !== totalFromReservations) {
// Correct aggregated count
await this.client.set(
`inventory:reserved:${variantId}`,
totalFromReservations.toString(),
);
}
}

Atomic Operations

Lua Script for Reservation

-- Atomic inventory reservation
-- KEYS[1] = inventory:reserved:{variantId}
-- KEYS[2] = inventory:reservation:{cartId}:{variantId}
-- ARGV[1] = quantity
-- ARGV[2] = availableInventory
-- ARGV[3] = ttlSeconds

local reservedKey = KEYS[1]
local reservationKey = KEYS[2]
local quantity = tonumber(ARGV[1])
local available = tonumber(ARGV[2])
local ttl = tonumber(ARGV[3])

-- Get current reserved count
local reserved = tonumber(redis.call('GET', reservedKey) or '0')

-- Check availability
if (available - reserved) < quantity then
return {'err', 'INSUFFICIENT_INVENTORY', available - reserved}
end

-- Increment reserved count
redis.call('INCRBY', reservedKey, quantity)

-- Create reservation
redis.call('SET', reservationKey, quantity, 'EX', ttl)

return {'ok', quantity}

Error Handling

Insufficient Inventory

if (availableInventory < requiredQuantity) {
throw new BadRequestException(
`Insufficient inventory. Available: ${availableInventory}, Required: ${requiredQuantity}`
);
}

Reservation Failures

try {
await this.reserveInventory(cartId, variantId, quantity);
} catch (error) {
// Rollback any partial reservations
await this.releaseInventory(cartId, variantId, quantity);
throw error;
}

Performance Considerations

Batch Operations

// Reserve inventory for multiple variants
async reserveMultiple(
cartId: string,
items: Array<{ variantId: string; quantity: number }>,
): Promise<void> {
// Use pipeline for batch operations
const pipeline = this.client.pipeline();

for (const item of items) {
pipeline.incrby(`inventory:reserved:${item.variantId}`, item.quantity);
pipeline.set(
`inventory:reservation:${cartId}:${item.variantId}`,
item.quantity.toString(),
"EX",
RESERVATION_TTL,
);
}

await pipeline.exec();
}

Monitoring

Inventory Metrics

// Track inventory operations
inventory_reserved_total: counter
inventory_committed_total: counter
inventory_released_total: counter
inventory_reservation_duration: histogram

Best Practices

  1. Reserve Early: Reserve inventory when checkout starts
  2. Release Promptly: Release reservations on payment failure
  3. Atomic Operations: Use Lua scripts for atomic reservations
  4. Reconciliation: Periodically reconcile reservations
  5. Error Handling: Handle reservation failures gracefully

Edge Cases

Concurrent Reservations

  • Lua scripts ensure atomic operations
  • Race conditions handled automatically
  • Last reservation wins if inventory insufficient

Reservation Expiration

  • Reservations expire after TTL
  • Inventory automatically released
  • Customer must restart checkout

Partial Inventory Available

  • Reserve available quantity
  • Show clear error message
  • Suggest alternatives if possible