Building a Flash Sale Engine That Cannot Oversell: Distributed Locking and Optimistic Concurrency in NestJS
How I used Redis distributed locking, atomic stock decrements, and TypeORM optimistic locking to guarantee zero overselling under thousands of simultaneous purchase requests, and the race condition I had to fix to get there.

The problem flash sales create
Flash sales create one of the most unforgiving concurrency problems in backend engineering. Thousands of people request the same limited stock in the same instant. If two requests both check inventory at the same moment, both see the item available, and both proceed to purchase it, the system oversells. The goal of this project was to make that structurally impossible, not just statistically unlikely.
nest-concurrency-engine is a flash sale purchase engine built with NestJS, designed to handle thousands of simultaneous purchase requests against limited inventory without a single unit being oversold. It uses three independent layers of protection, and the most important lesson from building it came from a bug that slipped past two of those three layers before I caught it.
The architecture
Layer one: Redis distributed locking
Before any request is allowed to touch inventory for a given item, it must acquire a lock scoped specifically to that item, using a key like lock:inventory:<itemId>. This is implemented with Redis's atomic SET key value NX PX ttl command, which creates the lock and assigns its expiry in a single indivisible operation. There is no unsafe window where the key exists without a timeout, which matters because a crash between a SET and a separate EXPIRE call would leave a permanent lock that nothing could ever clear.
Releasing the lock uses a Lua script that atomically checks a per-request ownership token before deleting the key. Without this, a request that gets delayed past its lock's expiry could resume and accidentally delete a different request's lock, one it no longer actually owns.
Lock acquisition retries briefly under contention instead of failing instantly. Since the work done under the lock takes milliseconds, a request that doesn't get the lock on its first attempt should usually succeed within a second of retrying, not be bounced immediately.
Layer two: synchronous admission inside the lock
This is the layer that exists because of a real bug I found during testing, not because I designed it perfectly the first time. My first implementation checked stock inside the lock, but deferred the actual decrement to an asynchronous Bull queue job that ran after the lock had already been released.
I ran my first concurrency test: 50 requests, 10 units of stock.
11 orders came back confirmed.
The cause was a timing gap. Request A would acquire the lock, see 1 unit available, create an order, enqueue a job, and release the lock, all before that queued job had actually run. Request B could then acquire the same lock moments later and also see 1 unit available, because the real decrement hadn't happened yet. Both requests passed the admission check legitimately. Both believed they had the last unit.
The fix was moving the stock check and the decrement into the same atomic step, inside the same lock, so nothing is released between deciding a unit is available and actually claiming it. Reran the test afterward: 50 requests, 10 units of stock, exactly 10 confirmed, every time.
Layer three: TypeORM optimistic locking
Every inventory row carries a @VersionColumn. Any update to that row includes its current version in the WHERE clause and increments it on success. If two writes somehow target the same row at nearly the same instant despite the Redis lock, only one can succeed, the other's version no longer matches and the update affects zero rows, which TypeORM surfaces as a version conflict.
This is the layer that doesn't depend on Redis behaving correctly. It is the final, database-enforced guarantee, independent of whatever happened upstream.
Why three layers instead of one
A single layer alone is fragile in a specific, predictable way. The Redis lock alone is fast but has no fallback if it fails. Optimistic locking alone is correct but would mean every single request hits Postgres directly under full contention, which doesn't scale. Together, the lock filters out nearly all contention before it ever reaches the database, and the version check is the guarantee that makes overselling structurally impossible, not just unlikely.
Proving it, not just claiming it
The actual point of this project isn't the architecture description, it's the test that verifies it. An automated Jest test creates an inventory item with 10 units of stock, fires 50 concurrent purchase requests against it, 5x oversubscription, and asserts that exactly 10 succeed.
HTTP status breakdown: { '201': 10, '409': 40 }
PASS test/concurrency.e2e-spec.ts
Ten confirmed. Forty correctly rejected. Zero oversold.
Tech stack
NestJS, TypeScript, PostgreSQL via TypeORM, Redis via ioredis, Bull queues, Docker Compose for local Postgres and Redis, Swagger for API documentation, Jest and Supertest for concurrency testing.
Closing thought
The gap between code that looks correct and code that is provably correct under load is where most concurrency bugs actually live. Building this taught me that more than any single line of the implementation did.
GitHub repository: https://github.com/PeaceMelodi/nest-concurrency-engine




