Packages
double_down
0.24.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
double_down
README.md
README.md
# DoubleDown
[](https://github.com/mccraigmccraig/double_down/actions/workflows/test.yml)
[](https://hex.pm/packages/double_down)
[](https://hexdocs.pm/double_down/)
Hexagonal architecture ports for Elixir — typed contracts, async-safe
stateful test doubles, and a built-in in-memory Repo that makes database-free
testing practical.
## The problem
Clean Architecture tells you to put domain logic behind port boundaries,
but in practice a couple of things get in the way: maintaining contract
behaviours and dispatch facades involves boilerplate that's tedious to keep
in sync, and unit-testing with complex dependencies like Ecto is hard enough
that most projects never do it - they just hit the database for every test
and accept the speed penalty and the inability to adopt property-based testing.
## What DoubleDown does
| Feature | Description |
|-------------------------------|------------------------------------------------------------------|
| Typed contracts | `defport` declarations with full typespecs |
| Contract behaviour generation | Standard `@behaviour` + `@callback` — Mox-compatible |
| Dispatch facades | `DoubleDown.Facade` generates config-dispatched caller functions |
| LSP-friendly docs | `@doc` tags on facade functions with types and parameter names |
| Async-safe test doubles | Process-scoped handlers via NimbleOwnership |
| Stateful test handlers | In-memory state with atomic updates and fallback dispatch |
| Dispatch logging | Record every call that crosses a port boundary |
| Built-in Repo contract | 15-operation Ecto Repo contract with stateless + in-memory impls |
## Terminology
If you're coming from Mox or standard Elixir testing, here's how
DoubleDown's terms map to what you already know:
| DoubleDown term | Familiar Elixir equivalent |
|---|---|
| **Contract** | Behaviour (`@callback` specs) — the interface an implementation must satisfy |
| **Facade** | The dispatch module (`def foo(x), do: impl().foo(x)`) — DoubleDown generates this |
| **Test double** | Mock/stub/fake — anything standing in for a real implementation in tests |
| **Port** | A contract + its facade — the boundary through which I/O operations pass |
See [Getting Started](docs/getting-started.md#terminology) for the
expanded version with test double types (mocks, stubs, fakes).
## Quick example
Define a port contract and facade in one module:
```elixir
defmodule MyApp.Todos do
use DoubleDown.Facade, otp_app: :my_app
defport create_todo(params :: map()) ::
{:ok, Todo.t()} | {:error, Ecto.Changeset.t()}
defport get_todo(id :: String.t()) ::
{:ok, Todo.t()} | {:error, :not_found}
defport list_todos(tenant_id :: String.t()) :: [Todo.t()]
end
```
Implement the behaviour:
```elixir
defmodule MyApp.Todos.Ecto do
@behaviour MyApp.Todos
@impl true
def create_todo(params), do: MyApp.Repo.insert(Todo.changeset(params))
@impl true
def get_todo(id) do
case MyApp.Repo.get(Todo, id) do
nil -> {:error, :not_found}
todo -> {:ok, todo}
end
end
# ...
end
```
Wire it up:
```elixir
# config/config.exs
config :my_app, MyApp.Todos, impl: MyApp.Todos.Ecto
```
Test with an in-memory test double — no database, full async isolation:
```elixir
# test/test_helper.exs
DoubleDown.Testing.start()
# test/my_app/todos_test.exs
defmodule MyApp.TodosTest do
use ExUnit.Case, async: true
setup do
DoubleDown.Testing.set_stateful_handler(
MyApp.Todos,
fn
:create_todo, [params], todos ->
todo = struct!(Todo, Map.put(params, :id, System.unique_integer()))
{{:ok, todo}, Map.put(todos, todo.id, todo)}
:get_todo, [id], todos ->
case Map.get(todos, id) do
nil -> {{:error, :not_found}, todos}
todo -> {{:ok, todo}, todos}
end
:list_todos, [_tenant], todos ->
{Map.values(todos), todos}
end,
%{} # initial state — empty store
)
:ok
end
test "create then get" do
{:ok, todo} = MyApp.Todos.create_todo(%{title: "Ship it"})
assert {:ok, ^todo} = MyApp.Todos.get_todo(todo.id)
end
test "get non-existent returns error" do
assert {:error, :not_found} = MyApp.Todos.get_todo(-1)
end
end
```
No Mox modules, no database, no sandbox — just a function that
maintains state. Each test process gets its own isolated state via
NimbleOwnership.
For Ecto-heavy code, DoubleDown also ships `Repo.InMemory` — a
ready-made stateful test double for the built-in Repo contract with
read-after-write consistency, `Ecto.Multi` support, and speeds suitable
for property-based testing. See [Repo](docs/repo.md).
## Documentation
- **[Getting Started](docs/getting-started.md)** — contracts, facades,
behaviours, config, dispatch resolution
- **[Testing](docs/testing.md)** — handler modes, dispatch logging,
async safety, process sharing, Mox compatibility
- **[Repo](docs/repo.md)** — built-in Ecto Repo contract, production
adapter, stateless and in-memory test doubles
- **[Migration](docs/migration.md)** — incremental adoption into
existing codebases, the two-contract pattern, coexisting with
direct Ecto.Repo calls
## Installation
Add `double_down` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:double_down, "~> x.y"}
]
end
```
Check [hex.pm/packages/double_down](https://hex.pm/packages/double_down) for the latest version.
Ecto is an optional dependency. If you want the built-in Repo contract,
add Ecto to your own deps.
## Relationship to Skuld
DoubleDown extracts the port system from
[Skuld](https://github.com/mccraigmccraig/skuld) (algebraic effects
for Elixir) into a standalone library. You get typed contracts,
async-safe test doubles, and dispatch logging without needing Skuld's
effect system. Skuld depends on DoubleDown and layers effectful dispatch
on top.
## License
MIT License - see [LICENSE](LICENSE) for details.