# Stateful Doubles
<!-- nav:header:start -->
[< Dispatch](dispatch.md) | [Up: Guides](../README.md) | [Index](../README.md) | [Double API >](double-api.md)
<!-- nav:header:end -->
Stateless doubles (stubs, module mocks) return canned responses — they
have no memory. Stateful doubles maintain in-memory state with atomic
updates, enabling read-after-write consistency and realistic behaviour
without a database.
## How state works
State lives inside `NimbleOwnership`, keyed by contract module. Each
handler is stored as a `DoubleDown.Dispatch.HandlerMeta.Stateful` struct
with an inline `:state` field. The dispatch machinery reads and writes
this state atomically via `NimbleOwnership.get_and_update` — every
`{result, new_state}` return from a handler is a single atomic operation.
Process isolation is handled by NimbleOwnership — each test process has
its own set of handlers and state, so `async: true` works without
cross-test interference.
## Handler arities
Stateful handlers come in two arities:
### 4-arity — own state only
```elixir
fn contract, operation, args, state -> {result, new_state} end
```
The handler receives its contract's current state and returns a new state.
This is the default for most fakes.
### 5-arity — cross-contract state access
```elixir
fn contract, operation, args, state, all_states ->
{result, new_state}
end
```
The 5th argument is a read-only snapshot of **all** contract states, keyed
by contract module. This enables the two-contract pattern where a queries
handler reads from the Repo's in-memory store:
```elixir
%{
DoubleDown.Repo => %{User => %{1 => %User{...}}},
MyApp.Queries => %{...},
}
```
A sentinel key (`DoubleDown.Contract.GlobalState`) detects accidental
returns of the global map — returning `all_states` instead of your own
state raises `ArgumentError`.
The global snapshot is taken before the `get_and_update` call — it's a
point-in-time view, not a live reference. Handlers can only update their
own contract's state via the return value.
## CanonicalHandlerState
When stateful handlers are installed via `DoubleDown.Double`,
Double wraps the user's state in a `CanonicalHandlerState` struct:
```
%CanonicalHandlerState{
fallback_state: user_state, # The user's domain state
expects: [...], # Ordered expect queue
stubs: %{op => [...]}, # Per-operation stubs
per_op_fakes: %{op => handler} # Per-operation fakes
}
```
This is an internal detail — user code only sees `fallback_state`
(extracted by `get_state/1`). The wrapping is what enables layered
expects/stubs/fakes over a stateful fallback: each sub-system (expect
queue, stub registry, fallback function) is tracked within the same
canonical state, allowing dispatch priority resolution inside a single
`get_and_update` call.
## State threading
State threads through sequenced operations:
```elixir
# Fallback writes to state
DoubleDown.Double.fallback(Contract, fn _c, :put, [k, v], state ->
{Map.put(state, k, v), Map.put(state, k, v)}
end, %{})
# Stateful expect reads what the fallback wrote
DoubleDown.Double.expect(Contract, :get, fn [k], state ->
{Map.get(state, k), state}
end)
```
After a 1-arity expect fires (stateless), the state is unchanged — the
next expect sees whatever state the fallback left it in.
## StatefulHandler behaviour
For reusable stateful fakes, implement `DoubleDown.Dispatch.StatefulHandler`:
```elixir
defmodule MyApp.InMemoryStore do
@behaviour DoubleDown.Dispatch.StatefulHandler
@impl true
def new(seed, _opts), do: seed
@impl true
def dispatch(_contract, :get, [id], state), do: {Map.get(state, id), state}
def dispatch(_contract, :put, [id, val], state), do: {:ok, Map.put(state, id, val)}
end
```
Implement either `dispatch/4` or `dispatch/5` (or both — `/5` takes
priority when both are defined). Modules implementing this behaviour can
be used directly with `Double.fallback`:
```elixir
DoubleDown.Double.fallback(MyContract, MyApp.InMemoryStore)
```
## StatelessHandler behaviour
For reusable stateless stubs, implement
`DoubleDown.Dispatch.StatelessHandler`:
```elixir
defmodule MyApp.TestStore do
@behaviour DoubleDown.Dispatch.StatelessHandler
@impl true
def new(fallback_fn, _opts) do
fn contract, operation, args ->
case {operation, args} do
{:get, [id]} -> %{id: id}
_ when is_function(fallback_fn) -> fallback_fn.(contract, operation, args)
_ -> raise "unhandled: #{operation}/#{length(args)}"
end
end
end
end
```
`new/2` receives an optional fallback function and options, and returns a
3-arity function `(contract, operation, args) -> result`. Used with
`Double.fallback` by module name:
```elixir
DoubleDown.Double.fallback(MyContract, MyApp.TestStore)
```
If a fallback function is also supplied, it's passed as the first argument
to `new/2`:
```elixir
DoubleDown.Double.fallback(MyContract, MyApp.TestStore,
fn _contract, :all, [User] -> [] end
)
```
<!-- nav:footer:start -->
---
[< Dispatch](dispatch.md) | [Up: Guides](../README.md) | [Index](../README.md) | [Double API >](double-api.md)
<!-- nav:footer:end -->