All engineering notes
Engineering · 11 min read

How Inventory Transactions Prevent Overselling

A database-first guide to stock deductions, concurrency, idempotent retries, and reconciliation for POS and inventory software.

By Ahmed Salar

Overselling happens when two requests both observe the same available stock and then both create a successful sale. The reliable fix is to perform the availability check and stock deduction inside one database transaction, protected by an appropriate row lock or atomic conditional update. Client-side checks can improve the interface, but they cannot guarantee correctness under concurrency.

The short answer: make the database the final authority

Overselling happens when two requests both observe the same available stock and then both create a successful sale. The reliable fix is to perform the availability check and stock deduction inside one database transaction, protected by an appropriate row lock or atomic conditional update. Client-side checks can improve the interface, but they cannot guarantee correctness under concurrency.

The transaction should also record the sale or inventory movement and return a clear success or failure state. If the request is retried, an idempotency key and a unique constraint should ensure the same operation is not applied twice.

Model stock movements and current balance separately

A current stock balance is useful for fast reads, while an immutable movement ledger explains how that balance changed. A purchase, sale, return, adjustment, or transfer should create a durable movement with the tenant, product, quantity, source, and operation identifier.

The balance can be updated in the same transaction as the movement. This gives the application a fast path for normal reads without losing the audit trail required for reconciliation. The exact schema depends on whether stock is tracked per warehouse, batch, serial number, or variant.

Which concurrency strategy should a POS use?

A conditional update such as reducing stock only when the current quantity is greater than or equal to the requested amount is often enough for a simple deduction. The application checks the affected-row count: one row means the deduction succeeded, and zero means the request must fail or enter a review state.

For workflows that update several related rows, a transaction with deliberate row-lock ordering can protect the invariant. Keep the transaction short, avoid network calls inside it, and make sure indexes support the lookup. Long transactions increase lock waits and make a busy store less predictable.

How do retries stay safe?

A scanner, browser, payment device, or network proxy may repeat a request after a timeout even though the original transaction committed. The client should send an idempotency key for the logical operation. The server stores that key with the tenant and result, and a unique constraint prevents a second successful write for the same key.

If the operation spans a message broker, use an outbox record written in the same transaction as the sale. A worker can publish the event and mark the outbox row complete. This prevents a committed inventory change from disappearing because publishing failed after the database commit.

Reconciliation is part of reliability

Even a well-designed system needs operational checks. Compare the current balance with the movement ledger, identify negative stock, track failed deductions, and review differences by store and product. Record who made manual adjustments and why.

Useful measurements include transaction failures, lock wait time, duplicate-key retries, queue age, reconciliation differences, and slow inventory queries. These signals turn an intermittent report of 'stock is wrong' into a concrete engineering investigation.

Written by Ahmed Salar, a Karachi-based full-stack software engineer focused on SaaS architecture, distributed systems, and reliable product engineering.

Explore related project work →