Skip to content

Published May 6, 2025

Preventing oversell: pessimistic locking at checkout

How to guarantee stock integrity under concurrent orders in a NestJS + TypeORM API, without sacrificing readability.

NestJSTypeORMMySQLConcurrencyE-commerce

Two customers buy the last item at the exact same moment. Without care, both orders go through: you have just sold stock you do not have. This is the classic race condition on stock.

Why a simple check is not enough

Read the stock, check it is positive, then decrement it: between the read and the write, another transaction can do exactly the same thing. The check is already stale by the time you write.

The pessimistic lock

At checkout I lock the stock row for the duration of the transaction. Any other transaction that wants the same row waits its turn:

await dataSource.transaction(async (manager) => {
  const item = await manager.findOne(StockItem, {
    where: { id },
    lock: { mode: "pessimistic_write" },
  });

  if (!item || item.quantity < requested) {
    throw new ConflictException("Insufficient stock");
  }

  item.quantity -= requested;
  await manager.save(item);
});

The read and the write are now atomic from the point of view of other transactions: oversell becomes impossible.

Pessimistic over optimistic?

Optimistic locking (version number + retry) shines when conflicts are rare. On a highly contended stock row, conflicts are frequent: the pessimistic lock avoids a storm of retries and keeps the code simple to read. The right tool depends on the contention profile.

The cost

A lock serialises access to the row: keep it as short as possible, and only take it on what needs it. In an order flow, that is a trade-off I gladly accept in exchange for the certainty of never overselling.

/ Contact

Software to design, deploy, operate?

Write to me. I reply within 48 h.