Skip to content

Understand: invariants as contracts, tests as behaviour

Validation in Bynk has two complementary shapes, and the difference is the universal/existential split:

  • Tests describe behaviourthere exists a case where this works. A test stands up the agent with controlled inputs and asserts about one run.
  • Invariants describe contractsfor all reachable states, this holds. An invariant is a claim the runtime enforces at each handler’s commit point.

A reader of an agent sees examples of its behaviour (via tests) and claims about its behaviour (via invariants), and the architecture binds both to the same handler-and-state machinery.

An agent already owns a piece of state and is the only thing that can change it (the single-owner rule). An invariant is the direct language-level expression of what domain-driven design calls an aggregate’s invariants — the consistency rules that must always hold for that aggregate. Putting them on the agent means:

  • they are visible at the contract boundary, not buried in handler bodies;
  • they compose with the failure model — a violation is a fault, not a typed outcome;
  • they reduce test burden — a property guaranteed by an invariant does not need a test case verifying it.
agent Inventory {
key sku: Sku
store available: Cell[Int]
invariant available_non_negative:
available >= 0
on call reserve(qty: Quantity) -> Effect[Result[(), ReserveError]] {
if (available < qty) {
Err(InsufficientStock)
} else {
let cur = available
available := cur - qty
Ok(())
}
}
}

The handler’s guard makes available >= 0 true by construction. The invariant captures that intent once, at the boundary — so a future refactor that reorders the guard, or a new handler that forgets it, fails at the commit boundary rather than silently persisting a negative balance. You do not write a test for “available never goes negative”; the invariant is that guarantee.

  • They are not cross-agent. A property like “the sum of reservations across all Inventory agents equals the original stock” is eventually-consistent and belongs to a saga or external monitoring — see the agent model.
  • They are not transactional rollback. A fault means the offending commit is never written, not that the whole handler is undone (effects already sent stand). See Agent invariants.
  • They are not a typed outcome. A violation is a fault the caller cannot pattern-match — it aborts, the same way any other fault does.