DEV Community

Cover image for How to Build Reliable Inventory Management Services in Node.js with Webhooks
Naresh Chandra Lohani
Naresh Chandra Lohani

Posted on

How to Build Reliable Inventory Management Services in Node.js with Webhooks

A warehouse API can report 120 units in stock while an order service reserves 15 units against an outdated quantity. If two orders arrive concurrently, both may succeed, leaving the warehouse with an oversold item. Delayed webhooks, duplicate events, and partial database failures make this problem harder to diagnose.

Inventory Management Services need to maintain consistent stock records across order management, warehouse operations, and external sales channels. This article explains how to process inventory events safely using Node.js, PostgreSQL, and a webhook-driven architecture.

For teams evaluating inventory and warehouse management solutions, the central engineering challenge is not receiving events. It is ensuring that every accepted event produces the correct inventory state, even when delivery is repeated or processing fails.

Context and Setup

A webhook-based inventory system typically contains four components:

  1. Order service: Publishes order creation, cancellation, and fulfillment events.
  2. Webhook receiver: Validates incoming requests and records events durably.
  3. Inventory service: Applies stock movements and reservation rules.
  4. Database: Stores stock balances, reservations, event identifiers, and transaction history.

Assume that an item has 20 available units. Two customers simultaneously attempt to reserve 12 units each. Without concurrency control, both requests might read the same initial balance and approve reservations totaling 24 units.

Inventory accuracy is a documented operational concern. A 2008 study by DeHoratius and Raman, published in Management Science, examined nearly 370,000 inventory records across 37 stores and found that 65% of the records were inaccurate. This result applies to the retailer studied, not every warehouse, but it illustrates why software teams should validate recorded quantities against physical inventory.

Prerequisites: Node.js 20 or later, PostgreSQL, an existing order source, and a webhook provider that supports signed requests and event identifiers.

Building Reliable Inventory Management Services

The recommended approach combines durable event ingestion, idempotent processing, and atomic stock updates. Each mechanism addresses a different failure mode.

Step 1: Define an Inventory Event Contract

Start by defining the fields required to process an inventory event. A stable event identifier helps the receiver distinguish a retry from a new business operation.

{
  "eventId": "evt_8f42",
  "type": "inventory.reservation_requested",
  "sku": "SKU-1042",
  "quantity": 3,
  "orderId": "ORD-5021"
}
Enter fullscreen mode Exit fullscreen mode

The receiver should validate the event type, SKU, positive integer quantity, order identifier, and request signature before accepting the event.

Do not trust a webhook simply because it arrived over HTTPS. Verify its signature against the original request body, enforce payload limits, and reject malformed events.

Step 2: Make Event Processing Idempotent

Idempotency means processing the same business operation repeatedly produces the same final result as processing it once.

Create an inventory_events table with a unique constraint on event_id. The database constraint, rather than an in-memory cache, should be the final authority because application instances can restart or process requests concurrently.

The following example uses PostgreSQL through the pg package:

import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL
});

export async function reserveInventory(event) {
  const client = await pool.connect();

  try {
    await client.query("BEGIN");

    // Why: the unique key prevents duplicate event processing.
    const inserted = await client.query(
      `INSERT INTO inventory_events (event_id)
       VALUES ($1)
       ON CONFLICT (event_id) DO NOTHING
       RETURNING event_id`,
      [event.eventId]
    );

    if (inserted.rowCount === 0) {
      await client.query("ROLLBACK");
      return { status: "duplicate" };
    }

    // Why: the conditional update prevents negative stock.
    const result = await client.query(
      `UPDATE inventory
       SET available = available - $1,
           reserved = reserved + $1
       WHERE sku = $2
         AND available >= $1
       RETURNING available, reserved`,
      [event.quantity, event.sku]
    );

    if (result.rowCount === 0) {
      throw new Error("Insufficient stock or unknown SKU");
    }

    // Why: event acceptance and stock changes commit together.
    await client.query("COMMIT");

    return { status: "reserved", stock: result.rows[0] };
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }
}
Enter fullscreen mode Exit fullscreen mode

This example assumes that inventory_events and inventory exist, the event has already been validated, and the event represents a new reservation. Production code should also persist the event's processing status and business outcome.

A duplicate event must not reserve stock twice. An unsuccessful reservation must not leave an event marked as successfully processed. Both properties depend on keeping the event record and inventory mutation within the same transaction.

Step 3: Choose a Retry and Recovery Strategy

Retries should handle transient failures without repeating completed business operations.

  1. Retry temporary database or network errors using exponential backoff and jitter.
  2. Treat validation failures and insufficient stock as business outcomes, not transient infrastructure errors.
  3. Route repeatedly failing events to a dead-letter queue for investigation.
  4. Record the event ID, SKU, order ID, attempt count, and failure reason in structured logs.
  5. Reconcile stock against authoritative warehouse counts and investigate discrepancies.

The database transaction protects local stock changes, but it cannot make a remote API call and a local database commit atomic. For workflows spanning multiple services, use a transactional outbox to publish events reliably and a saga or compensating action to handle failures across service boundaries.

For example, if a payment succeeds but inventory reservation fails, the order workflow may need to initiate a refund. Releasing a reservation after order cancellation must also be idempotent.

Real-World Application

For inventory-focused implementations at Oodles, this architecture provides a practical starting point for connecting warehouse operations with order management and external systems.

Consider a warehouse workflow receiving reservation requests from multiple sales channels. The engineering priorities are to prevent overselling, preserve an auditable movement history, and recover from duplicate or delayed events. Node.js can handle asynchronous request processing, while PostgreSQL transactions enforce stock invariants and a durable queue supports deferred retries.

The measurable acceptance criteria should include duplicate-event rejection rate, reservation conflict rate, event-processing latency at the 95th percentile, dead-letter queue volume, and inventory variance after reconciliation.

No verified Oodles project-specific performance figures are available for this example, so it would be misleading to claim a particular reduction in latency or inventory errors. A production rollout should establish a baseline and publish before-and-after measurements from actual traffic.

For related engineering and implementation work, visit Oodles.

Conclusion: Key Takeaways

  • Enforce invariants in the database: Conditional updates prevent concurrent reservations from exceeding available stock.
  • Use durable idempotency keys: Unique event identifiers prevent webhook retries from duplicating stock movements.
  • Separate technical failures from business failures: Retry transient errors, but handle insufficient stock through explicit workflow decisions.
  • Design for recovery: Dead-letter queues, structured logs, and reconciliation make failures diagnosable.
  • Measure production behavior: Track latency, duplicate events, reservation failures, and inventory variance before claiming improvements.

How does your team handle duplicate events and inventory reconciliation across multiple warehouses? Share your implementation approach or questions in the comments.

For help designing or improving Inventory Management Services, discuss your integration requirements with the Oodles team.

Frequently Asked Questions

1. What are Inventory Management Services in a distributed application?

Inventory Management Services are software components that track stock quantities, reservations, receipts, transfers, and adjustments. In distributed applications, they coordinate inventory changes across order systems, warehouses, and sales channels while maintaining consistent stock records and preventing conflicting updates.

2. How do you prevent duplicate inventory updates from webhooks?

Prevent duplicate inventory updates by assigning each business event a stable identifier and enforcing a database uniqueness constraint. Record the event and apply the corresponding stock mutation in the same transaction. Repeated deliveries should return the existing outcome rather than execute the inventory operation again.

3. How do you prevent overselling in Node.js?

Use atomic database operations instead of reading a stock balance and updating it separately. A conditional SQL update that checks available quantity while reserving stock prevents concurrent requests from reducing availability below zero. Transactions should also maintain reservation records and inventory movement history.

4. Should inventory webhook failures be retried automatically?

Retry temporary failures such as network timeouts and database unavailability using exponential backoff with jitter. Do not repeatedly retry permanent validation errors or insufficient-stock responses. After a configured attempt limit, route unresolved events to a dead-letter queue and provide a controlled replay process.

5. How should developers test an inventory integration?

Test duplicate deliveries, concurrent reservations, out-of-order events, database failures, service restarts, and partial failures between payment and fulfillment. Verify that stock never becomes negative, each event produces at most one business effect, and reconciliation identifies discrepancies between system records and physical counts.

Top comments (0)