manikanta.
← All workNext project →
Full stack · 2026Full stack

Anvil Hardware

A billing and inventory system for a single hardware outlet. Goods come in from suppliers, cashiers bill at the counter, and the owner sees what is selling and what is running out. The whole design is organised around one correctness problem that most systems this size get wrong.

Role
Solo — backend, frontend, deploy
Timeline
2026
Status
Live on Vercel and Render
GitHub ↗Live app ↗Demo login: owner / demo1234 · cashier / demo1234

Hosted on a free tier that sleeps after 15 minutes. The first request can take about 50 seconds to wake the API.

7
Database tables
3
Django apps
2
Roles, enforced server side
3
Tests on the billing rule
The problem

Two cashiers bill the last box of screws at the same moment. Under PostgreSQL's default READ COMMITTED isolation, both transactions read the same available quantity, both pass the stock check, and both write. Stock goes negative and the shop has sold something it does not have.

It is invisible in testing, because you have to hit it at exactly the wrong moment. So the fix cannot be a careful check in application code. The database itself has to refuse to let two transactions touch the same row at once.

Architecture
CLIENTReact 19 + Vite 8Bootstrap, JWT in an interceptorOwns UI state onlyvercel.appAPIDjango 5.1 + DRFaccounts — roles, JWTinventory — catalogue, receiptsbilling — the transactionEvery rule is enforced hereonrender.comDATAPostgreSQL 177 tables, PROTECT on FKsRow locks on billingrender.comJSON + JWTrows, or a 400Django ORMrowsONE BILL, ONE ATOMIC TRANSACTION01Sort product idsone global lock order02Lock the rowsSELECT … FOR UPDATE03Check, decrementline by line04Commit or discardall of it, or none
STAGE 01

Lock in a fixed order

Product ids are sorted, then locked with SELECT ... FOR UPDATE before any availability check. Sorting gives every transaction the same global lock order, so two bills touching the same pair of products cannot deadlock waiting on each other.

STAGE 02

Check, then decrement

Each line is validated against the locked row and stock is decremented as it passes. A second transaction waits at the lock rather than reading stale stock, then re-reads the committed state and correctly rejects the oversale.

STAGE 03

Commit, or discard all of it

The whole bill is one atomic block. If any line is short, the error rolls back every write above it, including lines that already succeeded. The cashier gets a 400 naming the product and the quantity actually available.

Evaluation
MeasureResultMethod
Oversell under concurrent billsPreventedSELECT … FOR UPDATE
Deadlock on shared productsAvoidedSorted lock ordering
Partial bill failureFull rollbackpytest, transaction=True
Goods receipt raceAvoidedF() expression, atomic
Stack
  • Python 3.12
  • Django 5.1
  • Django REST Framework
  • PostgreSQL 17
  • SimpleJWT
  • React 19
  • Vite 8
  • Bootstrap 5.3
  • pytest
  • Vercel
  • Render
Deployment

React front end on Vercel, Django API and PostgreSQL 17 on Render. Business rules live in a service module rather than the view, so the transaction boundary and the locking belong to the operation itself and can be tested or called outside an HTTP request. Tests run against PostgreSQL with real transaction boundaries, because SQLite accepts select_for_update and silently ignores it — the most important behaviour in the system would pass in development and fail in production.

What I'd do next

Want the walkthrough? I'll show you the repo.