I’m building a backend-only inventory system to practice real systems engineering fundamentals:
- state transitions
- invariants
- atomicity
- correctness under concurrency
No UI.
Just HTTP + database.
This week I finished the service + transport layer for:
PATCH /items/<uuid>/qty
This endpoint applies a delta-based transition to item quantity.
Why use delta instead of setting quantity directly?
Instead of:
qty = 4
we send:
{ "delta": -1 }
The system performs:
qty' = qty + delta
This allows the database to apply the change atomically and avoids read-modify-write race conditions.
The Service-Layer Implementation
All state mutation lives in the service layer.
updated = (
Item.objects
.filter(id=item_id, qty__gte=-delta)
.update(qty=F("qty") + delta)
)
What this does
1. Guard against invalid state
qty__gte=-delta
ensures:
current_qty + delta ≥ 0
If the result would be negative, the update never runs.
The database enforces this invariant.
2. Atomic update
qty = qty + delta
is executed entirely inside the database using F("qty").
No Python read-modify-write.
No race conditions.
3. Explicit outcome classification
If no row updated, we determine why:
if updated:
return Item.objects.get(id=item_id)
if not Item.objects.filter(id=item_id).exists():
return None
raise ValueError("qty cannot go below 0")
Service contract becomes:
| Return | Meaning |
|---|---|
| Item | success |
| None | not found |
| ValueError | invariant violated |
Transport maps to:
200 → success
404 → not found
400 → invalid transition
Bugs That Taught Me More Than the Feature
Bug 1 — Update succeeded but returned 404
Qty updated correctly but response returned “not found”.
Cause:
if updated:
return
Bare return → None.
View interpreted None as 404.
Fix:
if updated:
return Item.objects.get(id=item_id)
Lesson:
Return contracts must be explicit and deterministic.
Bug 2 — UUID routing mismatch
Model used UUID primary key.
URL used:
<int:item_id>
View never matched correctly.
Fix:
path("items/<uuid:item_id>/qty", ...)
Lesson:
URL converters define runtime identity types.
Bug 3 — Django HTML error pages
Invalid routes returned Django HTML debug pages.
Fix: custom JSON 404 handler.
def json_404(request, exception):
return JsonResponse({"error": "not_found"}, status=404)
Lesson:
Transport layer should always return consistent API responses.
Bug 4 — The boolean delta bug (subtle but dangerous)
This one looks small but is actually critical.
Python behavior
In Python:
isinstance(True, int) # True
bool is a subclass of int.
So if validation only checks:
if not isinstance(delta, int):
then this passes:
{ "delta": true }
What happens if you accept it?
True → behaves like 1
False → behaves like 0
So:
{ "delta": true }
becomes:
qty = qty + 1
This creates a hidden mutation path where boolean values mutate state.
Even worse:
qty__gte=-delta
If delta=True:
-True == -1
The guard becomes:
qty >= -1
Almost always true.
So a boolean silently becomes an increment operation.
That violates the API contract and creates extremely hard-to-trace bugs.
The fix
Reject bool explicitly:
if isinstance(delta, bool) or not isinstance(delta, int):
raise ValueError("delta must be an int")
Now the system only accepts real integers.
Lesson
Type validation must be explicit at the transport boundary.
Silent coercion creates invisible state transitions.
Bug 5 — Missing transaction boundary
If update succeeded but later code failed, state could commit partially.
Fix:
with transaction.atomic():
This guarantees:
all-or-nothing transition
Example Invariant Violation
Current state:
qty = 0
Request:
{ "delta": -2 }
Would produce:
qty = -2
Database blocks update.
Response:
400
{ "error": "qty cannot go below 0" }
State remains unchanged.
Architecture That Emerged
transport → service → model → database
Transport
- parse JSON
- validate shape
- map service results → HTTP
Service
- normalize input
- enforce invariants
- perform atomic transitions
Database
- source of truth
- guard state
Biggest Takeaway
Correctness comes from:
- explicit transitions
- database guards
- deterministic return contracts
- atomic updates
Not clever abstractions.
Today the system became predictable.
Next Steps
- remove csrf_exempt
- auth boundary
- request logging
- concurrency tests
If you’re learning backend systems, try implementing an atomic delta update yourself.
The bugs you hit will teach you more than the feature ever will.
Top comments (0)