← Back to blog

Your Code Works. Until Two Users Click “Buy” at the Same Time.

Databases: How to ensure nothing breaks when two or more requests compete to modify a piece of data at the same time.

Imagine this.

You’re building an e-commerce platform.

There’s one iPhone left in stock.

Two customers have been refreshing the page all morning, waiting for it.

Then it happens.

Customer A clicks “Buy.”

Almost at the exact same moment…

Customer B clicks “Buy.”

Your backend receives both requests.

Request A checks the database.

stock = 1

Perfect. Continue.

Request B checks the database.

stock = 1

Also perfect. Continue.

Request A creates an order.

Request B creates an order.

Congratulations.

You just sold one iPhone to two people.

And the scary part?

Your code might be completely “correct.”

The Bug That Doesn't Look Like a Bug

You could have written something like:

javascript
const product = await getProduct(productId); if (product.stock > 0) { await createOrder(userId, productId); await updateStock(productId, product.stock - 1); }


Read the stock.

Check availability.

Create the order.

Reduce the stock.

Looks reasonable.

And with one customer, it works beautifully.

The problem appears when multiple requests execute concurrently.

Both requests can read the same state before either has updated it.

This is a classic race condition.

And adding another if statement won't save you.

“Easy. Put Redis in Front of It.”

That’s often where the conversation goes next.

Use Redis.

Create a distributed lock.

Only allow one request through at a time.

And yes, Redis can absolutely play a role in concurrency control, especially when coordination needs to happen across multiple application instances.

But there's a more important question first:

Who actually owns the stock?

If PostgreSQL is your source of truth, your database already has concurrency-control mechanisms designed for exactly this problem.

Sometimes the safest solution is also surprisingly small.

Instead of:

Read stock → check stock → update stock

make the database perform the condition and update atomically.

For example:

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

Then check how many rows were updated.

1 row?

You got the item.

0 rows?

Someone beat you to it.

No separate availability check based on potentially stale information.

No opportunity for two requests to independently decide that the same final item belongs to them.

For workflows involving multiple related operations, you can go further with transactions and row-level locking, such as SELECT ... FOR UPDATE, or carefully chosen transaction isolation levels.

The exact solution depends on the workflow.

The principle doesn't.

Concurrency Changes the Question

When we're learning backend development, we often think:

“Does this code produce the correct result?”

Production systems force you to ask a harder question:

“Does this code still produce the correct result when 100 requests execute it at the same time?”

Payments.

Ticket reservations.

Wallet balances.

Coupon redemptions.

Seat bookings.

Inventory.

Anywhere two users can compete to modify the same piece of state, concurrency matters.

And that’s why knowing PostgreSQL syntax isn't quite the same thing as understanding PostgreSQL.

The interesting part begins when two perfectly valid requests arrive at exactly the wrong time.

Because sometimes the hardest backend bugs aren't the ones that crash your server.

They're the ones that return 200 OK twice.