View Source Bond.Server (Bond v1.13.2)

Design by Contract for GenServer process state.

Bond's struct @invariant constrains every value of a type, but the state most worth constraining in Elixir is the state that changes over time inside a process. Bond.Server brings contracts to that state: declare module-wide properties of a server's state and Bond checks them automatically around the server's state-transition callbacks.

defmodule Counter do
  use GenServer
  use Bond.Server

  @state_invariant non_negative: state.count >= 0

  @impl true
  def init(n), do: {:ok, %{count: n}}

  @impl true
  def handle_call(:inc, _from, state), do: {:reply, :ok, %{state | count: state.count + 1}}
end

A @state_invariant is checked after every state-transition callback returns a new state — init/1, handle_call/3, handle_cast/2, handle_info/2, handle_continue/2, and code_change/3 — and a violation raises Bond.InvariantError (with :kind :state_invariant). Unlike a struct @invariant, this fires even when a callback mutates state inline (the common case), because Bond wraps the callbacks themselves rather than relying on the state flowing through a contracted pure function.

Usage order

use GenServer must come before use Bond.Server. Bond.Server detects the callbacks you define via the @on_definition compiler hook, which only sees definitions made after it is installed; use GenServer provides default callback implementations during its own expansion, so putting it first keeps those defaults out of Bond.Server's view.

The module must also genuinely implement the GenServer behaviour, whether through use GenServer or a bare @behaviour GenServer. Bond identifies callbacks by name and arity, so in a module that is not a GenServer an ordinary init/1 or handle_info/2 helper would be indistinguishable from a real callback. Rather than guess, Bond checks the behaviour: in a non-GenServer module no callback is wrapped, and any @state_invariant or @transition_invariant produces a compile-time warning that it cannot be checked.

Transition invariants

A @transition_invariant relates the prior state to the next state across a transition, via the implicit old_state and new_state bindings:

@transition_invariant monotonic: new_state.count >= old_state.count

It is checked across every transition callback — handle_call/3, handle_cast/2, handle_info/2, handle_continue/2 — and a violation raises Bond.InvariantError (with :kind :transition_invariant). A transition invariant is what the Design by Contract literature calls a history constraint (Liskov & Wing). init/1 and code_change/3 are treated as re-creations: they establish a new state (checked by @state_invariant) but have no comparable prior state, so transition invariants do not apply to them.

Like Bond's other contracts, @state_invariant and @transition_invariant checks honour the :invariants configuration and runtime gate: both share the :invariants kind, so they observe the precondition ≤ postcondition ≤ invariant chain, are compiled out entirely under invariants: :purge, and can be toggled at runtime with Bond.Config.enable/1/disable/1. See Bond and Bond.Config.