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.
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.