Skip to main content

Command Palette

Search for a command to run...

Authorization in Django: From Permissions to Policies : Part 10 — Invariants: What the System Must Never Allow

Where Rules End and Guarantees Begin

Updated
5 min read
Authorization in Django: From Permissions to Policies : Part 10 — Invariants: What the System Must Never Allow

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 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.

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:

  1. Database constraints —Uniqueness constraints preventing duplicate payments for the same order, regardless of the write path.

  2. Transaction boundaries — Atomic updates ensuring inventory is fully reserved or unchanged—never partially applied.

  3. Model-level guarantees — Guardrails preventing modification once a record reaches a terminal state.

  4. 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
Who may attempt an action
Stable, declarative, capability-based

Policies
What is valid now
Contextual, expressive, domain-aware

Invariants
What must always be true
Absolute, enforced, non-negotiable

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

  1. Eric Evans (2003) — Domain-Driven Design: Tackling Complexity in the Heart of Software — Addison-Wesley
    https://www.domainlanguage.com/ddd/

  2. Martin Fowler (2003) — Patterns of Enterprise Application Architecture — Addison-Wesley
    https://martinfowler.com/books/eaa.html

  3. Martin Kleppmann (2017) — Designing Data-Intensive Applications — O’Reilly Media
    https://dataintensive.net/

  4. 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/

  5. Leslie Lamport (1977) — Proving the Correctness of Multiprocess Programs — IEEE Transactions on Software Engineering
    https://lamport.azurewebsites.net/pubs/proving.pdf

  6. Django Software Foundation — Database Transactions — Django Documentation
    https://docs.djangoproject.com/en/stable/topics/db/transactions/

Thinking in Django & DRF

Part 6 of 13

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.

Up next

Authorization in Django: From Permissions to Policies — Part 9 — Policies: Making Context Explicit

From Capability to Contextual Validity

More from this blog

A

Abhilash PS — Engineering Thought & Software Architecture

23 posts