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.
Hosted on a free tier that sleeps after 15 minutes. The first request can take about 50 seconds to wake the API.
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.
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.
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.
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.
| Measure | Result | Method |
|---|---|---|
| Oversell under concurrent bills | Prevented | SELECT … FOR UPDATE |
| Deadlock on shared products | Avoided | Sorted lock ordering |
| Partial bill failure | Full rollback | pytest, transaction=True |
| Goods receipt race | Avoided | F() expression, atomic |
- Python 3.12
- Django 5.1
- Django REST Framework
- PostgreSQL 17
- SimpleJWT
- React 19
- Vite 8
- Bootstrap 5.3
- pytest
- Vercel
- Render
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.
- Add sale reversal. Bills currently cannot be voided or amended without separate compensating transaction logic.
- Model tax properly: GST rates, HSN codes and tax inclusive pricing are all out of scope today.
- Add a stock adjustment path, so damage, theft and stocktake corrections have somewhere to go.
- Keep an audit trail on the catalogue, since price and reorder level changes currently overwrite in place.