Packages
double_down
0.61.0
0.69.0
0.68.0
0.66.0
0.65.0
0.64.1
0.64.0
0.63.3
0.63.2
0.63.1
0.63.0
0.62.1
0.61.0
0.60.4
0.60.3
0.60.2
0.60.1
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.1
0.48.0
0.47.2
0.47.1
0.47.0
0.46.3
0.46.2
0.46.1
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.1
0.41.0
0.40.0
0.39.0
0.38.0
0.37.2
0.37.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.1
0.30.0
0.29.0
0.28.1
0.28.0
0.27.0
0.26.0
0.24.0
Builds on the Mox pattern — generates behaviours and dispatch facades from `defcallback` declarations — and adds stateful test doubles powerful enough to test Ecto.Repo operations without a database.
Current section
Files
Jump to
Current section
Files
docs/boundaries.md
# Boundaries
<!-- nav:header:start -->
[< DoubleDown](../README.md) | [Up: Guides](../README.md) | [Index](../README.md) | [Dispatch >](dispatch.md)
<!-- nav:header:end -->
DoubleDown separates _what you call_ from _what handles the call_. A function
call passes through four layers:
```
┌──────────────────────────────────────────────────────┐
│ FUNCTION CALL │
│ MyApp.Repo.insert(changeset) │
└──────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ 1. CONTRACT │
│ (type-level interface) │
│ │
│ ┌────────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ defcallback │ │ @behaviour │ │ any module │ │
│ │ (explicit, │ │ (explicit, │ │ via Dynamic │ │
│ │ typed, rich) │ │ existing │ │ Facade │ │
│ │ │ │ module) │ │ (implicit) │ │
│ └────────┬───────┘ └──────┬───────┘ └──────┬──────┘ │
└───────────┼────────────────┼────────────────┼────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ 2. FACADE │
│ (generated dispatch functions) │
│ │
│ ┌────────────────┐ ┌───────────────┐ ┌─────────────┐ │
│ │ContractFacade │ │BehaviourFacade│ │DynamicFacade│ │
│ │use │ │use │ │setup(Mod) │ │
│ │ ContractFacade │ │BehaviourFacade│ │shim │ │
│ │defcallback... │ │ │ │ │ │
│ └────────┬───────┘ └──────┬────────┘ └──────┬──────┘ │
└──────────┼────────────────┼─────────────────┼────────┘
└────────────────┼─────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ 3. DISPATCH │
│ (call resolution) │
│ │
│ ┌────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Static │ │ Runtime │ │ Test │ │
│ │ │ │ Config │ │ Handler │ │
│ │ compile- │ │ │ │ │ │
│ │ time │ │ call_config │ │ call/4 │ │
│ │ inlined │ │ /4 │ │ │ │
│ │ direct │ │ │ │ NimbleOwner- │ │
│ │ call │ │ App.get_env │ │ ship lookup │ │
│ │ │ │ → apply/3 │ │ → handler │ │
│ │ (zero │ │ │ │ │ │
│ │ overhead) │ │ │ │ │ │
│ └─────┬──────┘ └───────┬──────┘ └───────┬──────┘ │
└────────┼─────────────────┼─────────────────┼─────────┘
└─────────────────┼─────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ 4. IMPLEMENTATION │
│ (actual execution) │
│ │
│ ┌──────────────────────────┐ ┌───────────────────┐ │
│ │ Production Module │ │ Test Double │ │
│ │ │ │ │ │
│ │ @behaviour Contract │ │ Module handler │ │
│ │ def operation(...) │ │ Stateless fn │ │
│ │ │ │ Stateful fn │ │
│ │ e.g. MyApp.EctoRepo │ │ Double.expect/ │ │
│ │ │ │ fallback/stub │ │
│ └──────────────────────────┘ └───────────────────┘ │
└──────────────────────────────────────────────────────┘
── Static path (prod, compile-time config): Facade → Static → Production
── Config path (prod, no compile config): Facade → Config → Production
── Test path (test env): Facade → Test → Test Double
── DynamicFacade test path: Facade → Test → Test Double
── DynamicFacade no-handler (passthrough): Facade → (handler not found)
→ original module
```
## Layer 1: Contract
The contract is the type-level interface — it defines _what_ operations exist and
their type signatures, but not _how_ they're implemented.
### defcallback (explicit contract)
Use `defcallback` from `DoubleDown.Contract` when you control the interface.
It generates `@behaviour` + `@callback` + introspection metadata
(`__callbacks__/0`) from a single macro call. `defcallback` requires named
parameters (`id :: String.t()`) — these appear in generated `@spec` and `@doc`
on the facade, giving LSP-friendly hover docs at every call site.
```elixir
defmodule MyApp.Todos do
use DoubleDown.Contract
defcallback get_todo(tenant_id :: String.t(), id :: String.t()) ::
{:ok, Todo.t()} | {:error, term()}
defcallback list_todos(tenant_id :: String.t()) :: [Todo.t()]
end
```
### @behaviour (existing module)
Any existing Elixir `@behaviour` module works as a contract — see
`DoubleDown.BehaviourFacade`. Use this for behaviours you don't control:
third-party libraries, existing codebase behaviours, or any module with
`@callback` declarations.
### Implicit (DynamicFacade)
With `DoubleDown.DynamicFacade`, no contract module exists at all — the target
module's public API becomes the contract implicitly. The module is shimmed at
test time via bytecode replacement, so any call can be intercepted.
## Layer 2: Facade
The facade is what callers actually invoke. It generates wrapper functions for
each contract operation that delegate to the dispatch layer.
### ContractFacade
For `defcallback` contracts. Supports combined contract + facade (one module)
or separate modules. Options control dispatch behaviour at compile time.
```elixir
# Combined contract + facade
defmodule MyApp.Todos do
use DoubleDown.ContractFacade, otp_app: :my_app
defcallback get_todo(id :: String.t()) :: {:ok, Todo.t()} | {:error, term()}
end
```
### BehaviourFacade
For vanilla `@behaviour` modules. The behaviour must be compiled before the
facade — they live in separate modules.
```elixir
defmodule MyApp.Todos.Facade do
use DoubleDown.BehaviourFacade, behaviour: MyApp.Todos.Behaviour, otp_app: :my_app
end
```
### DynamicFacade
Mimic-style bytecode interception. Call `setup/1` in `test_helper.exs` before
`ExUnit.start()`. The module is backed up and replaced with a dispatch shim.
Tests that don't install a handler get the original module's behaviour.
```elixir
# test/test_helper.exs
DoubleDown.DynamicFacade.setup(MyApp.EctoRepo)
ExUnit.start()
```
## Layer 3: Dispatch
Dispatch resolves which implementation handles a given call. The resolution
strategy is selected at compile time per-facade via options.
### Static dispatch
When `static_dispatch?: true` and the implementation is available in config at
compile time, the facade function calls the implementation module directly.
No `Application.get_env`, no `NimbleOwnership` — the call inlines to identical
bytecode as calling the impl directly. Default in `:prod`.
### Runtime config dispatch
`DoubleDown.Dispatch.call_config/4` reads `Application.get_env(otp_app, contract)[:impl]`
and calls `apply(impl, operation, args)`. Used in production when static
dispatch isn't available, and in non-prod when `test_dispatch?: false`.
### Test handler dispatch
`DoubleDown.Dispatch.call/4` checks NimbleOwnership for a process-scoped test
handler before falling back to config. This is the default in non-prod
environments — it's what makes `DoubleDown.Double.fallback/2`, `expect/3`, etc.
work in tests.
### DynamicFacade dispatch
`DynamicFacade.dispatch/3` checks NimbleOwnership for a test handler, falling
back to the original (backed-up) module. This is always the path for
a DynamicFacade (which is only ever set up under test) — there's no config-based resolution because there's no contract
module to configure.
## Layer 4: Implementation
The implementation is the module or function that actually executes the
operation.
### Production
A module implementing the contract's `@behaviour`, wired via config:
```elixir
# config/config.exs
config :my_app, MyApp.Todos, impl: MyApp.Todos.Ecto
```
### Test doubles
Installed via `DoubleDown.Double` (recommended) or `DoubleDown.Testing`:
- **Module handler** — delegates to a module via `@behaviour`
- **Stateless handler** — `fn contract, operation, args -> result end`
- **Stateful handler** — `fn contract, operation, args, state -> {result, new_state} end`
(4-arity) or `fn contract, operation, args, state, all_states -> {result, new_state} end`
(5-arity, with cross-contract state access)
See `DoubleDown.Double` for the recommended API and
`DoubleDown.Dispatch.StatefulHandler` for the behaviour.
<!-- nav:footer:start -->
---
[< DoubleDown](../README.md) | [Up: Guides](../README.md) | [Index](../README.md) | [Dispatch >](dispatch.md)
<!-- nav:footer:end -->