DEV Community

Cover image for How to Build Inventory Management Services That Stay Consistent Under Concurrency
Naresh Chandra Lohani
Naresh Chandra Lohani

Posted on

How to Build Inventory Management Services That Stay Consistent Under Concurrency

An inventory API can return the wrong stock count even when every individual query looks correct. The problem usually appears when multiple orders reserve the same SKU at nearly the same time, while warehouse updates, cancellations, and payment retries are also modifying inventory.

This is where Inventory Management Services need more than CRUD endpoints. The service must make stock changes atomic, make retries safe, and provide a clear audit trail for every movement.

In this guide, we will build that design around Node.js, PostgreSQL, and Docker, with Redis and AWS as optional infrastructure components. For teams evaluating an implementation, Oodles provides inventory and warehouse management solutions for systems that require inventory synchronization across operational workflows.

Context and Setup

The core architecture is a transactional inventory service sitting between sales channels and warehouse operations.

Web / Mobile / Marketplace
          |
       API Gateway
          |
    Inventory Service
      |          |
 PostgreSQL     Redis
      |
 Inventory Ledger
      |
 Warehouse / ERP / Shipping
Enter fullscreen mode Exit fullscreen mode

The important design decision is that PostgreSQL remains the source of truth for stock mutations. Redis can accelerate reads or absorb temporary traffic, but it should not independently decide whether a reservation is valid.

For a useful performance reference, AWS documented a PostgreSQL pgbench test using Amazon EBS configurations. Its reported transaction throughput ranged from 5,686 TPS on gp2 to 6,956 TPS on io2 for that particular workload and environment. These figures are benchmark-specific, not guarantees for an inventory workload.

That distinction matters. Inventory workloads should be benchmarked using their own transaction shape, concurrency, indexes, and contention patterns.

Inventory Management Services: Transaction-Safe Stock Reservation

Step 1: Model stock as a transactional resource

Start with a table that represents the current available quantity and another table that records movements.

CREATE TABLE inventory (
  sku_id BIGINT PRIMARY KEY,
  available INTEGER NOT NULL CHECK (available >= 0)
);

CREATE TABLE inventory_movements (
  id BIGSERIAL PRIMARY KEY,
  sku_id BIGINT NOT NULL,
  quantity INTEGER NOT NULL,
  reason TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);
Enter fullscreen mode Exit fullscreen mode

Why two tables?

The inventory row provides a fast current-state lookup. The movement table provides historical context for receiving, reservation, release, adjustment, and shipment events.

This also makes reconciliation possible. If the current quantity differs from the expected quantity calculated from movements, the discrepancy can be investigated instead of silently overwriting it.

Step 2: Lock the SKU during reservation

The reservation operation should read and modify the same row inside one database transaction.

PostgreSQL's FOR UPDATE locks selected rows against conflicting updates until the transaction ends. PostgreSQL also supports SKIP LOCKED, which can be useful for queue-like workloads where workers should avoid waiting on already-locked rows.

A simplified Node.js implementation using pg looks like this:

async function reserveStock(client, skuId, quantity) {
  await client.query("BEGIN");

  try {
    // Why: row lock prevents concurrent reservations from reading stale stock.
    const result = await client.query(
      `SELECT available
       FROM inventory
       WHERE sku_id = $1
       FOR UPDATE`,
      [skuId]
    );

    if (result.rowCount === 0 || result.rows[0].available < quantity) {
      throw new Error("Insufficient inventory");
    }

    // Why: update happens inside the same transaction as the locked read.
    await client.query(
      `UPDATE inventory
       SET available = available - $1
       WHERE sku_id = $2`,
      [quantity, skuId]
    );

    // Why: the ledger records why the quantity changed.
    await client.query(
      `INSERT INTO inventory_movements (sku_id, quantity, reason)
       VALUES ($1, $2, $3)`,
      [skuId, -quantity, "reservation"]
    );

    await client.query("COMMIT");
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

The transaction is deliberately small. Do not call external payment, shipping, or warehouse APIs while holding the database lock.

Step 3: Make retries idempotent

A network timeout does not tell the client whether the reservation succeeded. Retrying blindly can therefore reserve the same quantity twice.

Add an idempotency key to reservation requests:

POST /inventory/reservations
Idempotency-Key: order-8472-item-01
Enter fullscreen mode Exit fullscreen mode

Store that key with the resulting reservation. A repeated request returns the original result instead of performing another stock mutation.

AWS describes idempotent APIs as a way to make retries safe by allowing repeated requests to produce no additional side effects.

The trade-off is additional state and cleanup. Idempotency records need a retention policy, and the database must enforce uniqueness on the key.

This pattern is preferable to relying exclusively on client-side retry logic because the inventory service owns the business invariant.

Real-World Application

In an Oodles implementation of an inventory platform, this architecture can be applied to a system handling SKU reservations across storefront and warehouse workflows. The implementation pattern uses PostgreSQL for transactional stock state, an inventory movement ledger for traceability, Node.js APIs for reservations, and asynchronous workers for downstream warehouse synchronization.

For production reporting, the meaningful metrics should include reservation p95 latency, database lock wait time, transaction rollback rate, duplicate-request rate, and inventory reconciliation discrepancies.

Because project-specific production measurements are not included in the supplied brief, production numbers should not be fabricated. Instead, an implementation review should establish a baseline first, then report before-and-after measurements from the actual environment.

Teams looking at the broader architecture can also review Oodles for related engineering and warehouse-management capabilities.

Conclusion / Key Takeaways

  • Treat stock mutation as a transaction, not as a sequence of independent API operations.
  • Lock only the inventory rows that must change so unrelated SKUs can continue processing concurrently.
  • Use an inventory ledger to preserve the reason and history behind every quantity change.
  • Make reservation APIs idempotent so network retries cannot create duplicate reservations.
  • Benchmark the real workload, including lock contention and concurrent reservations, rather than treating generic database TPS as an application guarantee.

Discuss the Architecture

How do you handle concurrent reservations, inventory reconciliation, and retry safety in your systems? Share your approach in the DEV.to comments.

For architecture discussions or implementation requirements, contact Inventory Management Services.

FAQ

1. What are Inventory Management Services?

Inventory Management Services are software components that track stock quantities, reservations, warehouse movements, adjustments, and availability. A production implementation normally combines transactional database operations, APIs, audit records, synchronization workflows, and monitoring to keep inventory state consistent across operational systems.

2. How do you prevent two orders from reserving the same stock?

Use a database transaction with row-level locking or an equivalent atomic update. In PostgreSQL, SELECT ... FOR UPDATE can lock the inventory row while the application validates and changes available quantity. This prevents conflicting transactions from simultaneously using the same stock state.

3. Should Redis be the source of truth for inventory?

Usually, no. Redis can provide fast cached availability or support temporary coordination, but transactional inventory state should generally reside in a durable database. The database should determine whether a reservation succeeds, while caches are invalidated or updated after committed changes.

4. Why are idempotency keys important for inventory APIs?

Idempotency keys prevent repeated requests from applying the same business operation multiple times. If a client times out after creating a reservation, it can retry using the same key. The server can then return the existing reservation instead of decrementing inventory again.

5. How should inventory performance be measured?

Measure application-specific metrics such as p50 and p95 reservation latency, transactions per second, lock wait duration, rollback rate, database CPU, connection-pool saturation, and reconciliation errors. Generic database benchmarks are useful references, but they should not replace workload-specific testing.

Top comments (0)