Backend Development 12 min read

Designing a High‑Availability Inventory System: Stock Pre‑allocation, Duplicate Order Prevention, Rollback Mechanisms, and Concurrency Control

The article explains how JD Daojia's inventory system achieves stability and high availability for millions of items by using health‑monitoring platforms, choosing an order‑submission stock reservation strategy, preventing duplicate submissions with token and idempotent mechanisms, implementing rollback procedures, and applying various concurrency‑safe stock‑deduction techniques with code examples.

JD Tech
JD Tech
JD Tech
Designing a High‑Availability Inventory System: Stock Pre‑allocation, Duplicate Order Prevention, Rollback Mechanisms, and Concurrency Control

Inventory System Stability and High Availability

After more than two years of online operation and technical iteration, JD Daojia's inventory system now serves tens of thousands of merchants and hundreds of thousands of stores. Stability is achieved through a robust underlying platform that provides 24/7 health monitoring of applications, JVM, Docker containers, and physical machines, and by coupling data analysis with business requirements to validate and maximize the benefits of each iteration.

When to Pre‑occupy (or Deduct) Stock

How should stock be reserved when a user places an order?

Example: a product has 1,000 units, and 1,000 users each intend to purchase 1,000 units simultaneously. Three possible schemes are discussed:

Scheme 1 – Reserve stock when the user adds the item to the cart (only one user could add the product).

Scheme 2 – Reserve stock at order submission (only one user can successfully submit the order; others receive an "out of stock" message).

Scheme 3 – Reserve stock after payment success (all users can create orders, but only one will succeed in payment; the rest are cancelled).

JD Daojia adopts Scheme 2 because cart‑level reservation would block genuine buyers, while Scheme 3 would cause a massive number of cancelled orders and a poor user experience. Statistics show that the proportion of orders submitted but not paid is very small, and the system automatically releases reserved stock after 30 minutes.

Duplicate Order Submission Issues

Repeated order submissions can lead to severe inventory over‑deduction.

Typical scenarios include:

Benign user behavior – the user clicks the "Submit Order" button again because the backend response is delayed.

Malicious behavior – attackers repeatedly call the order‑submission API.

System retry – the order service retries the stock‑deduction request after a timeout.

Solutions:

Disable the submit button after the first click (frontend gray‑out).

Use a token mechanism: each checkout page issues a unique token ID; the order service validates the token and allows processing only when the token exists and its usage count equals 1.

Ensure idempotent backend APIs by attaching the order number to each stock‑deduction call and using a distributed lock. Example implementation:

int ret = redis.incr(orderId);
redis.expire(orderId, 5, TimeUnit.MINUTES);
if (ret == 1) {
    // First time processing this order ID
    boolean alreadySuccess = alreadySuccessDoOrder(orderProductRequest);
    if (!alreadySuccess) {
        doOrder(orderProductRequest);
    }
} else {
    return "Operation failed: duplicate submission";
}

Inventory Rollback Mechanisms

Rollback is required in several scenarios:

User cancels before payment.

User cancels after payment.

Risk control system cancels an abnormal order.

Coupled system failure – e.g., the order service calls points, inventory, and coupon services; if the coupon service fails after the others succeed, a rollback of points and inventory is needed.

For scenarios 1‑3, the order center publishes an MQ message on cancellation, and each downstream system consumes the message to perform its own rollback. Scenario 4 is more complex because the order has not been created; the order service must actively invoke rollback APIs of the inventory and coupon services, which must be idempotent. If the order service crashes during rollback, the inventory and coupon services must perform self‑health checks and execute pending rollbacks via a worker that periodically scans orders older than a configurable threshold (e.g., 40 minutes) and verifies their status before rolling back.

Concurrent Stock Deduction for High‑Traffic Scenarios

How to safely deduct stock when many users attempt to buy the same item simultaneously?

Two pseudo‑code approaches are presented.

Approach 1 uses a synchronized block to serialize all requests, which guarantees correctness but suffers from low throughput.

synchronized(this) {
    long stockNum = getProductStockNum(productId);
    if (stockNum > requestBuyNum) {
        int ret = updateSQL("update stock_main set stockNum=stockNum-" + requestBuyNum + " where productId=" + productId);
        if (ret == 1) {
            return "Deduction succeeded";
        } else {
            return "Deduction failed";
        }
    }
}

Approach 2 moves the concurrency control into the SQL statement by adding a condition that the remaining stock must be greater than or equal to the requested amount.

int ret = updateSQL("update stock_main set stockNum=stockNum-" + requestBuyNum + " where productId=" + productId + " and stockNum>=" + requestBuyNum);
if (ret == 1) {
    return "Deduction succeeded";
} else {
    return "Deduction failed";
}

For flash‑sale (seckill) scenarios, an additional rate‑limiting counter can be introduced before the DAO layer to reject a configurable percentage of requests, reducing database pressure.

public class SeckillServiceImpl {
    private long count = 0;
    public String buy(User user, int productId, int productNum) throws InterruptedException {
        count++;
        if (count % 2 == 1) {
            Thread.sleep(1000);
            return "Purchase failed";
        } else {
            return doBuy(user, productId, productNum);
        }
    }
}

To prevent a single user from purchasing the same item multiple times, a Redis key per user‑product pair is used. The key is incremented on each attempt; only the first increment (value 1) is allowed to proceed.

public String doBuy(User user, int productId, int productNum) {
    int tmp = redis.incr(user.getUid() + productId);
    if (tmp == 1) {
        redis.expire(user.getUid() + productId, 3600); // key expires after 1 hour
        doBuy1(user, productId, productNum);
        return "Purchase succeeded";
    } else {
        return "Purchase failed";
    }
}

The article concludes by inviting readers to join JD's engineering team, emphasizing that inventory systems present many challenging high‑concurrency problems worth exploring.

BackendConcurrencyInventoryIdempotencystock management
JD Tech
Written by

JD Tech

Official JD technology sharing platform. All the cutting‑edge JD tech, innovative insights, and open‑source solutions you’re looking for, all in one place.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.