Invariants

An invariant is a consistency rule that must always hold for an aggregate — reserved stock never exceeds available stock, a line is only added while its order is open. You declare the rule once, name the operations it guards, and attach it to an entity with invariants [...]. The platform enforces it on every write, so the rule cannot be forgotten at a call site.

Invariants come in two shapes, distinguished by what they need to see.

A simple invariant: over the entity's own fields

When a rule only involves the row being written, it needs nothing external. Chowk's stock rule says reserved units can never exceed available units:

sub-domains/inventory/invariants/stock_covers_reservations.vishwakarma
invariant StockCoversReservations "Reserved stock can never exceed available stock." {
  expr       "this.reserved <= this.available"
  enforce_on [create, update]
  target     persistence
  error      "InsufficientStock"
  message    "Reserved stock cannot exceed available stock."
}

The entity opts in by naming it:

entity StockLevel aggregate_root global {
  // …
  invariants [StockCoversReservations]
}

Three clauses shape it. expr is the condition over this (the row). enforce_on [create, update] names the operations it guards. target persistence is the interesting one: because the rule only reads the entity's own fields, it can be pushed all the way down into the datastore — it becomes a database CHECK constraint plus a trigger that raises the typed InsufficientStock error. The rule then holds even against a write that bypasses the application entirely; there is no path, app or raw SQL, that can leave stock in an inconsistent state.

A cross-aggregate invariant: pulling in context

Some rules span more than one aggregate. Whether a line may be added to an order depends on the order's status — a different aggregate than the line being written. A per-row condition cannot see that, so the invariant fetches it with a context_query:

sub-domains/ordering/invariants/order_open_to_add_line.vishwakarma
invariant OrderOpenToAddLine "A line can be added only while its order is still open (PENDING)." {
  expr          "ctx.status == 'PENDING'"
  context_query GetOrder
  context_input { id: "this.order_id" }
  key_field     id
  error         "OrderNotOpen"
  message       "Cannot add a line: the order is not open."
  enforce_on    [create]
  target        application
}

Read it as a small pipeline:

  • context_query GetOrder — the query that supplies the context (here, the parent order).
  • context_input { id: "this.order_id" } — how to call it: pass the new line's order_id as the query's id.
  • key_field id — the correlation key that ties the fetched row back to the write.
  • expr "ctx.status == 'PENDING'" — the condition, now over ctx (the fetched order) rather than this (the line).

Because it dispatches an application-layer query — something SQL cannot do — this invariant declares target application. Add a line to an open order and it succeeds; add one to an order that has moved to PAID, and the write is rejected with the typed OrderNotOpen error (a 409 conflict). The line's own row never has to know the order's status; the invariant fetches it and decides.

The two shapes at a glance

Simple invariantCross-aggregate invariant
Readsthe entity's own fields (this)another aggregate, via context_query (ctx)
Extra clausescontext_query, context_input, key_field
Typical targetpersistence (a DB CHECK + trigger)application (an app-layer query)
ExampleStockCoversReservationsOrderOpenToAddLine

Both are declared once, attached by name, and enforced on every operation you list in enforce_on — so a consistency rule lives in exactly one place, with a typed error, instead of being re-checked (and eventually forgotten) in each command that touches the aggregate.