Ensuring Stock Accuracy: Understanding Concurrency in ESHOPMAN's Inventory Reservations

As an e-commerce migration expert at Move My Store, we constantly monitor the ESHOPMAN community for insights that empower our users. A recent technical discussion has brought to light a crucial aspect of managing inventory reservations within ESHOPMAN, particularly for developers building custom integrations or extending core functionalities via the Admin API.

This insight focuses on the potential for stock oversubscription and data desynchronization when making direct, concurrent calls to ESHOPMAN's Inventory Module service methods, specifically IInventoryService.createReservationItems(). It's vital to understand that standard ESHOPMAN workflows, such as cart completion and Admin API reservation flows, are robustly protected and do not exhibit this behavior. This discussion is primarily for those interacting with the Inventory Module at a lower level.

The Challenge: Concurrent Reservation Calls and Stock Oversubscription

When custom application code directly invokes IInventoryService.createReservationItems() concurrently for the same inventory item, without proper locking mechanisms, two primary issues can arise:

  • Overselling: More ReservationItem records can be created than the actual available stock allows. This means if you have one unit of an item, two separate, simultaneous requests could both successfully reserve it.
  • Data Inconsistency: The reserved_quantity counter, which aggregates reserved stock, can become out of sync with the actual number of live reservation items. This can lead to a "lost update," where the counter only increments once despite two reservations being made. In extreme cases, subsequent deletions of reservation items can cause reserved_quantity to become negative, even when no items are reserved.

Reproducing the Scenario for ESHOPMAN Developers

For ESHOPMAN developers, understanding this behavior is key. You can observe this by executing two direct, concurrent reservation calls for the same inventory item and location:

const reserve = () =>
  inventoryModuleService.createReservationItems({
    inventory_item_id,
    location_id,
    quantity: 1,
  })

const result = await Promise.allSettled([reserve(), reserve()])

In such a scenario, if only one unit is available, both calls might succeed, creating two reservation records while the reserved_quantity counter only reflects one unit reserved.

The Technical Root Cause in ESHOPMAN's Node.js/TypeScript Core

The core of this issue lies in a "check-then-act" pattern within the Inventory Module's Node.js/TypeScript implementation. The availability check (ensureInventoryLevels) reads the available_quantity without acquiring a row-level lock. This means two concurrent transactions can both pass the check before either transaction writes its reservation. Similarly, the update to reserved_quantity is a read-modify-write operation that can suffer from lost updates when transactions race.

It's important to note that other ESHOPMAN modules, like the Promotion Module, employ explicit PostgreSQL FOR UPDATE row locks for similar check-then-act patterns, ensuring serialization and preventing such race conditions.

ESHOPMAN's Built-in Protection: The Locking Module

A critical takeaway for ESHOPMAN developers is that the platform already provides a robust solution for this: the ESHOPMAN Locking Module. Standard ESHOPMAN flows, including those for cart completion and Admin API-driven reservations, wrap reservation creation calls within this module, using the inventory_item_id as the locking key.

When you wrap your custom reservation creation logic in the Locking Module, only one caller can acquire the lock and proceed, effectively preventing oversubscription:

await locking.execute([inventory_item_id], async () => {
  return await inventoryModuleService.createReservationItems(...)
})

API Contract and Best Practices for ESHOPMAN Developers

This discussion raises an important question for the ESHOPMAN team and developers: what is the intended API contract for createReservationItems? Should callers explicitly manage locking, or should the service method inherently provide concurrency safety?

For now, the best practice for ESHOPMAN developers creating custom solutions that interact directly with the Inventory Module is to:

  • Always consider concurrency: Assume that direct service method calls might be subject to race conditions.
  • Utilize the ESHOPMAN Locking Module: Employ the built-in locking mechanism to ensure atomicity and prevent stock oversubscription in your custom code.
  • Monitor ESHOPMAN updates: The ESHOPMAN team is investigating this behavior, considering potential enhancements like adding pessimistic locking within the service, clarifying API documentation, or providing higher-level, concurrency-safe primitives.

This community insight underscores the power and flexibility of ESHOPMAN as a headless commerce platform, built on Node.js/TypeScript and integrated with HubSpot. It also highlights the importance of understanding its underlying architecture when building advanced custom solutions. By adhering to best practices and leveraging ESHOPMAN's built-in tools, developers can ensure the integrity of their storefronts deployed via HubSpot CMS and provide a seamless experience for merchants and customers alike.

Start with the tools

Explore migration tools

See options, compare methods, and pick the path that fits your store.

Explore migration tools