DEV Community

Cover image for What Happens When Two Users Buy the Last Item at the Same Time?
Alistair Rowan Whitcombe
Alistair Rowan Whitcombe

Posted on

What Happens When Two Users Buy the Last Item at the Same Time?

Imagine an ecommerce store where only one item is left in stock.

Two users open the same product page.
Both see “In stock”.
Both click Buy almost at the same moment.

Who actually gets the item?

This situation reveals a classic backend problem that shows up in real production systems: race conditions and inventory consistency.

The core problem: race conditions

A race condition occurs when multiple requests read and update the same data concurrently, and the final result depends on timing.

In an inventory system, the sequence often looks like this:

Request A reads stock = 1
Request B reads stock = 1
Request A places order and updates stock to 0
Request B places order and updates stock to -1

From the system’s point of view, both orders looked valid when they were created.

This is how overselling happens.

Why frontend checks are not enough

You might think this can be solved by disabling the Buy button or refreshing stock on the frontend.

Platforms like Shopperdot do apply frontend protections such as disabling buttons and showing loading states, but these are only for user experience.

Frontend checks cannot prevent:

  • Multiple devices
  • Multiple tabs
  • Network delays
  • Direct API calls

The backend must be responsible for enforcing correctness.

A naive backend implementation

A common mistake is checking stock first, then updating it.

Example in pseudocode:

const product = await db.products.findById(productId);

if (product.stock > 0) {
  await db.orders.insert({ productId });
  await db.products.update(productId, {
    stock: product.stock - 1
  });
}

Enter fullscreen mode Exit fullscreen mode

This code looks reasonable, but under concurrent requests it is unsafe.

Between reading and updating the stock, another request can sneak in.

Approach 1: database-level locking

One solution is to lock the row while updating stock.

Example using a transactional approach:

BEGIN;

SELECT stock FROM products
WHERE id = 42
FOR UPDATE;

-- if stock > 0
UPDATE products
SET stock = stock - 1
WHERE id = 42;

COMMIT;

Enter fullscreen mode Exit fullscreen mode

The FOR UPDATE lock ensures that only one transaction can modify that row at a time.

Other transactions must wait until the first one finishes.

Approach 2: atomic updates

Another common approach is to let the database decide whether the update is valid.

UPDATE products
SET stock = stock - 1
WHERE id = 42 AND stock > 0;

Enter fullscreen mode Exit fullscreen mode

Then check how many rows were affected.

If zero rows were updated, the item was already sold out.

This pattern avoids race conditions without explicit locks and is widely used in high-traffic systems, including inventory handling at Shopperdot.

Approach 3: optimistic locking

Optimistic locking assumes conflicts are rare and detects them when they happen.

Example:

UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 42 AND version = 7;

Enter fullscreen mode Exit fullscreen mode

If the version does not match, the update fails and the operation is retried or rejected.

This works well when contention is low.

What the user experiences

From the user’s perspective:

  • One user successfully places the order
  • The other sees “Out of stock” or a failure message

Even though both clicked Buy, only one purchase is accepted.

This behavior is intentional and correct.

Why this matters in real systems

Inventory bugs lead to:

  • Overselling
  • Manual refunds
  • Broken trust
  • Customer support overhead

Well-designed systems treat inventory updates as critical sections, not casual database writes.

At Shopperdot, inventory consistency is handled entirely on the backend so that timing, latency, or repeated clicks never result in selling more items than exist.

Final takeaway

When two users try to buy the last item at the same time, the system must decide who wins.

That decision should be enforced by:

  • Atomic operations
  • Database locks or conditions
  • Clear failure handling

Concurrency is not an edge case. It is the default state of real-world systems.

If you design for concurrency, your system stays correct even under pressure.

Top comments (0)