Two concurrent checkouts can both observe the final unit as available before either decrements it. Row locking fixes that race, but locking overlapping carts in inconsistent order introduces deadlocks.
ShopNest
A commerce system engineered for correctness under concurrency — atomic inventory, idempotent checkout, refresh-token theft detection, and order history that survives catalogue changes.
Product thinking, engineering decisions, system structure, and implementation.

The problem
What made this worth solving?
A storefront is easy until two customers try to buy the final unit at the same moment. Commerce correctness lives in races around inventory, duplicate checkout requests, authentication sessions, and preserving historical order data after products change.
Engineering approach
How the system was shaped.
The project uses a Turborepo monorepo with a NestJS API and Next.js frontend sharing OpenAPI-generated TypeScript contracts. Money is represented as integer cents rather than floating point. Order items snapshot purchase-time data so later catalogue changes cannot rewrite history, while checkout correctness is enforced inside database transactions rather than through application-level reads.
- 01NestJS 11
- 02Next.js 15
- 03TypeScript
- 04Prisma
- 05PostgreSQL 17
- 06Turborepo
- 07Argon2id
- 08GitHub Actions
Hardest technical problem
The part that needed real engineering.
The cart acts as the serialization point, product rows are acquired in deterministic order, and stock is decremented with a conditional update only when enough quantity remains. A zero-row update means another transaction won, so the request fails safely instead of allowing negative inventory.
Outcome
What the work produced.
Concurrency-sensitive behaviour is covered by automated tests, and important architectural decisions are documented explicitly instead of being left implicit in implementation details.
Looking back
What I would change today.
One authentication path originally revoked a compromised token family and then threw inside the same transaction, causing the revocation to roll back. The fix reinforced an important boundary: transactions should return a decision; control flow should happen after the transaction commits.
Design the behaviour. Engineer for what happens next.