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

Search for a command to run...
Where Rules End and Guarantees Begin

No comments yet. Be the first to comment.
Thinking in Django & DRF is a series about Django & DRF by understanding why things are designed the way they are. It explores insights from mastering Django & DRF, like syntax, shortcuts, abstraction, invariants, and architectural boundaries.
Tracing a Single Request Through Authority, Validity, and State
When we first learn Django, models are usually explained in a very concrete way. One model maps to one database table, the relationship is one-to-one, and each row represents an instance of that model. This mental model works well for most use cases ...
In a Django REST Framework application, how should responsibilities be divided? When we build a DRF APIs, we touch many layers: models for saving data, serializers for validating input, views for endpoints, and sometimes extra structure like services...
Definition Conceptual abstraction is a long-standing principle in software design—one that appears wherever systems are expected to survive change. When conceptual abstraction is discussed in the context of REST, it can sometimes feel like a REST-spe...

Definition When we design software systems, we often discuss rules, validations, and best practices. These concepts are familiar and useful, but they operate at the surface level of system behavior. Beneath all of them lies a much stronger idea—one t...
On Boundaries, Guarantees, and the Limits of Authorization

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.
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.
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 open and closed
Inventory 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.
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.
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.
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.
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.
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.
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.
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.
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.html
Martin 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.pdf
Django Software Foundation — Database Transactions — Django Documentation
https://docs.djangoproject.com/en/stable/topics/db/transactions/