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
endOnce 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:
- Requests for a specific product arrive in parallel.
- The broker orders them into a single FIFO (First-In, First-Out) stream.
- 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.