# 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.