Authorization in Django: From Permissions to Policies : Part 10 — Invariants: What the System Must Never Allow
Where Rules End and Guarantees Begin

By now, the structure is clear.
Permissions answer who may attempt.
Policies answer what is valid now.
Even together, they are not enough.
A system can pass every permission check and every policy gate and still reach an impossible state. That responsibility belongs to the final layer: invariants.
Policies govern decisions and the Invariants govern reality.
The Limit of Policy
Policies are conditional. They answer a question at a specific moment:
Given the current state and context, should this action proceed?
This makes them effective. They evaluate context before an action occurs. Their limit is that they cannot guarantee the correctness of the state that follows.
System failures rarely come from bad intent. They arise from invariant violations—from invalid states becoming representable.
Examples are common:
Two concurrent requests both close the same order
Inventory drops below zero under load
A finalized record is partially updated
A workflow skips a mandatory state
Each can pass a policy check. None should ever exist.
This gap is not a policy failure. It is an invariant failure.
What an Invariant Is
An invariant is a condition that must always hold true—before, during, and after every operation.
Not “usually true.”
Not “true when rules are followed.”
Always true.
Examples:
An order cannot be both
openandclosedInventory quantity cannot be negative
A payment cannot exist without an order
A finalized document cannot change
A workflow cannot skip required states
Invariants define the shape of the system’s valid state space. They do not reason about actors or timing. They declare what is possible at all.
Why Invariants Are Not Authorization
It is tempting to treat invariants as strict policies. This is a mistake.
— Authorization asks: May this actor attempt this action?
— Policies ask: Is this action valid in the current context?
— Invariants ask: Is this state representable in the system?
When a permission or policy fails, the system denies an action. When an invariant fails, the system itself is wrong.
That difference changes how failures are handled:
Permission failures → 403 / 404
Policy failures → 403 / 409
Policy violations return 409 Conflict when a valid request collides with the system’s current state; 422 Unprocessable Entity applies only to semantic validation of input, not to policy decisions.Invariant failures → errors, rollbacks, alerts
Invariant violations are not user errors. They are architectural faults.
Where Invariants Are Enforced
Invariants are enforced at the point of state mutation, not in access checks.
Typical locations include:
Database constraints —Uniqueness constraints preventing duplicate payments for the same order, regardless of the write path.
Transaction boundaries — Atomic updates ensuring inventory is fully reserved or unchanged—never partially applied.
Model-level guarantees — Guardrails preventing modification once a record reaches a terminal state.
Domain services for irreversible transitions — Explicit transition logic enforcing valid state progressions (for example, draft → approved → published) and rejecting all others.
These guarantees must hold even when policies are bypassed, code paths are incorrect, workers retry, or requests arrive concurrently under load.
That is what makes invariants architectural rather than procedural.
A Concrete Example
Consider inventory reduction.
A policy may check whether enough stock exists. That does not prevent two concurrent transactions from both succeeding.
The invariant is stronger:
Inventory quantity must never be negative.
In Django, this belongs at the mutation boundary:
from django.db import transaction
from django.db.models import F
from django.core.exceptions import ValidationError
def reserve_inventory(item_id, quantity):
with transaction.atomic():
updated = (
Inventory.objects
.filter(id=item_id, quantity__gte=quantity)
.update(quantity=F("quantity") - quantity)
)
if updated == 0:
raise ValidationError("Inventory invariant violated")
No permission logic. No policy logic. Just a state guarantee.
Either the invariant holds—or the operation fails.
Invariants as System Contracts
An invariant is a promise the system makes to itself:
No matter how this operation is invoked, this state will never exist.
That promise simplifies everything else:
Policies no longer need defensive checks
Workflows assume valid prior states
Background jobs remain safe
External systems can trust guarantees
When invariants are weak, complexity leaks upward. Every layer becomes cautious. Systems become brittle.
The Three Layers, Precisely Scoped
At this point, the model stabilizes:
Permissions | Policies | Invariants |
None replaces the others. Each exists because the others cannot perform its role.
Permissions → Policies → Invariants
This is not layering for elegance. It is responsibility isolation.
Why Invariants Are Easy to Miss
Invariants remain invisible in small systems.
They surface only when:
Concurrency increases
State transitions multiply
Background processing appears
Integrations depend on guarantees
By then, failures are no longer local bugs—they are structural defects.
Identifying invariants early is not over-engineering. It is a signal that the system is being designed to endure.
Where We Go Next (Part 11 Preview)
We now have all three components—clearly separated, precisely scoped.
In the next part, Part 11, we will walk through a complete workflow from request to mutation, showing how permissions, policies, and invariants cooperate without collapsing into one another.
Not as theory, but as architecture in motion.
Bibliography / References
Eric Evans (2003) — Domain-Driven Design: Tackling Complexity in the Heart of Software — Addison-Wesley
https://www.domainlanguage.com/ddd/Martin Fowler (2003) — Patterns of Enterprise Application Architecture — Addison-Wesley
https://martinfowler.com/books/eaa.htmlMartin Kleppmann (2017) — Designing Data-Intensive Applications — O’Reilly Media
https://dataintensive.net/Jim Gray, Andreas Reuter (1993) — Transaction Processing: Concepts and Techniques — Morgan Kaufmann
https://www.microsoft.com/en-us/research/publication/transaction-processing-concepts-and-techniques/Leslie Lamport (1977) — Proving the Correctness of Multiprocess Programs — IEEE Transactions on Software Engineering
https://lamport.azurewebsites.net/pubs/proving.pdfDjango Software Foundation — Database Transactions — Django Documentation
https://docs.djangoproject.com/en/stable/topics/db/transactions/



