Prevent Inventory Race Conditions in E-Commerce

High-concurrency events such as flash sales often cause multiple customers to attempt purchasing the same limited stock simultaneously. Without proper concurrency controls, this creates race conditions that lead to overselling, data corruption, and negative user experiences. This article outlines the primary software engineering strategies—including database locking, atomic operations, distributed locks, and asynchronous queues—used to maintain data integrity during inventory reservations in concurrent e-commerce systems.

1. Atomic Conditional Updates (Optimistic Concurrency)

The simplest and often most performant approach at the database level is using atomic conditional updates. Instead of reading the stock, calculating the difference in the application layer, and saving it back, the check and deduction happen within a single SQL statement:

UPDATE inventory 
SET stock = stock - 1 
WHERE product_id = 123 AND stock >= 1;

Because the database engine handles row-level mutations sequentially, the query checks the current stock at the moment of update. If the database returns an affected row count of 1, the reservation succeeded. If it returns 0, the item is out of stock. This avoids application-level race conditions without holding open long locks.

2. Pessimistic Locking

Pessimistic locking explicitly blocks access to a database row until the active transaction completes. In relational databases, this is typically executed using a SELECT ... FOR UPDATE statement:

BEGIN TRANSACTION;

SELECT stock FROM inventory WHERE product_id = 123 FOR UPDATE;

-- Application checks if stock > 0, then updates:
UPDATE inventory SET stock = stock - 1 WHERE product_id = 123;

COMMIT;

Any subsequent request attempting to read or modify that specific row must wait until the first transaction commits or rolls back. While highly reliable and simple to implement, pessimistic locking can degrade performance under heavy load, causing connection pool exhaustion and database deadlocks.

3. Redis and Atomic Lua Scripting

To offload high-traffic concurrency from relational databases, teams frequently use in-memory data stores like Redis. Because Redis is single-threaded in its command execution, operations executed sequentially remain race-free.

Using an atomic Lua script ensures that the read, evaluation, and decrement happen in an unbroken cycle on the Redis server:

local stock = tonumber(redis.call('get', KEYS[1]))
if stock and stock > 0 then
    redis.call('decr', KEYS[1])
    return 1 -- Reservation success
else
    return 0 -- Out of stock
end

Once reserved in Redis, the system can persist the reservation asynchronously to the primary relational database, shielding it from traffic spikes.

4. Distributed Locks

In distributed microservice architectures where stock updates involve multi-step workflows across distinct services, row locks and localized transactions are insufficient. Distributed locks synchronize access across disparate systems using a shared lock provider such as Redis (via algorithms like Redlock) or Apache ZooKeeper.

Before processing a reservation, a service must acquire a uniquely identified lock for that specific SKU, execute the validation and hold operation, and immediately release the lock. Distributed locks must always include a Time-To-Live (TTL) expiration to prevent permanent deadlocks in the event that a service worker crashes mid-transaction.

5. Message Queues and Request Serialization

Another robust approach is to convert concurrent requests into a serialized stream using a message broker like Apache Kafka, RabbitMQ, or AWS SQS.

When a customer clicks "Reserve," the system writes an event to a partitioned queue, using the product_id as the partition key. Because messages within the same partition are consumed sequentially:

  1. Requests for a specific product arrive in parallel.
  2. The broker orders them into a single FIFO (First-In, First-Out) stream.
  3. A single consumer processes each reservation one by one, completely eliminating concurrency-related race conditions.

6. Time-Limited Reservation Patterns

To prevent stock from being tied up by abandoned carts, reservations should be treated as temporary states rather than permanent deductions. When an inventory item is reserved, a record is created with an expiration timestamp (e.g., 10 minutes).

If the payment succeeds, the reservation converts to an order. If the time expires or the user cancels, a background sweeper or a time-delayed queue event automatically increments the available stock back into the pool.