# Vicander

A Domain-Driven Design framework for Elixir, built on behaviours and macros: entities,
aggregates and value objects that **can never exist in an invalid state**, specifications,
commands and queries, a transactional event outbox, projections, and an MCP server so an AI
client can use the application. Its modules are `DDD.*`. The example application that uses it is
`todo_app` (a sibling project, `../todo_app`).

Field rules and specification invariants run on `new`, `update` and every business operation.

## Installation

```elixir
# mix.exs
def deps do
  [
    {:vicander, "~> 0.1"},
    # the database driver is yours to choose; PostgreSQL is the one the framework supports
    {:postgrex, ">= 0.0.0"}
  ]
end
```

```elixir
# .formatter.exs: the DSL (`field :name, :string`) is written without parentheses
[import_deps: [:ecto, :ecto_sql, :vicander], inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]]
```

## Database

The framework depends on `ecto_sql` but not on a driver: your application adds the one it needs
(`postgrex`, ...), and `DDD.Dialect` follows from the repo's adapter. Almost everything is an
Ecto query, so it runs on whatever the repo runs on; the few things that differ between databases
are behind the `DDD.Dialect` behaviour:

| Ecto adapter             | Dialect                | Status    |
|--------------------------|------------------------|-----------|
| `Ecto.Adapters.Postgres` | `DDD.Dialect.Postgres` | supported (14 and later) |

For another adapter, implement `DDD.Dialect` (`lock_for_claim/1`, `now/1`, `read_only/2`,
`columns/2`) and set `config :vicander, dialect: MyApp.Dialect`. A repo with no dialect raises an
`ArgumentError` naming its adapter.

What is not portable yet:

* lists of plain values (`{:array, :string}`) are native Postgres arrays, and the `:ilike` filter
  is Postgres's `ILIKE`;
* the generated outbox migration has partial indexes (`where:`), which MySQL does not have.

Leases and retry times are compared with the **database's** clock (`c:DDD.Dialect.now/1`), so nodes
whose clocks differ still agree on when a lease is over. Nested entities, value objects and lists
of them are one `jsonb` column each.

## Setup

1. **Tell it your repo.** Commands, queries, handlers, projections and the `mix vic.*` tasks use
   the repo you configure, unless one names another with `repo:`:

   ```elixir
   # config/config.exs
   config :vicander, repo: MyApp.Repo
   ```

2. **Create the outbox tables**, once, and one migration per aggregate:

   ```bash
   mix vic.gen.outbox_migration
   mix vic.gen.migration MyApp.Domain.Order
   mix ecto.migrate
   ```

3. **Start the framework** in your supervision tree, after the repo. It starts the event bus,
   subscribes the handlers and projections you list, and runs the outbox poller and the
   dead-letter handler (`poller: false` for a node that should not deliver events, and in tests):

   ```elixir
   children = [
     MyApp.Repo,
     {DDD.Supervisor, handlers: [MyApp.Handlers.SendWelcome, MyApp.Projections.OrderSummary]}
   ]
   ```

The framework starts nothing by itself, and reads nothing from your application's own
configuration. See `DDD.Config` and `DDD.Supervisor`.

## Developing the framework

```bash
mix deps.get
mix test          # creates and migrates the test database; needs Postgres on localhost
mix credo --strict   # style and complexity (config in .credo.exs); dev only
mix dialyzer         # types; the first run builds a cache in priv/plts (a few minutes)
```

Postgres defaults for the tests live in `config/test.exs` (`postgres`/`postgres` on
`localhost`). The tests use `DDD.TestRepo` and the fixtures in `test/support`; the outbox tables
come from `test/support/repo/migrations`. Test output is quiet by default (warnings and errors
only). `LOG=1 mix test` shows the whole flow, and `LOG=1 mix test path/to/file_test.exs:LINE`
follows a single test:

```
[command] MyApp.Commands.CompleteItem started %{item_id: ..., list_id: ...}
[op] MyApp.Domain.TodoItem.complete ok
[op] MyApp.Domain.TodoList.complete_item ok
[repo] saved MyApp.Domain.TodoList <id> (1 event(s) written to the outbox)
[command] MyApp.Commands.CompleteItem ok in 7.8ms
[outbox] published MyApp.Events.ItemCompleted (MyApp.Domain.TodoList <id>)
[event_bus] MyApp.Events.ItemCompleted -> 1 handler(s): MyApp.Handlers.AchievementTracker
```

`[op]` and `[event_bus]` lines are `debug`, the rest `info`. Failed commands log the error codes
(`[command] ... failed in 3.1ms: title_blank (MyApp.Domain.TodoItem.new) ...`). SQL queries are
not shown unless you also set `SQL=1` (`LOG=1 SQL=1 mix test ...`). Tests that assert on a log
line use `capture_log`, so those stay captured.

## Project standards

* No compile warnings (`warnings_as_errors` is on).
* At least 90% test coverage (`mix test --cover` fails below the threshold).
* One module per file. In `lib/ddd` the file is named after its module and sits in a folder for
  its concern; the module names stay short (`DDD.Entity`), so the folders do not appear in them
  (`test/ddd/layout_test.exs` checks this):

  ```
  lib/ddd/
    model/         entity, aggregate, value_object, schema, identity, type, money,
                   specification, error, bounded_context, domain_service
    use_cases/     command, query, params, filter, listing, page, slice
    events/        domain_event, event_bus, event_handler, outbox/ (poller, dead letters)
    persistence/   repository, in_memory_repository, repository/ecto, migration, projection,
                   serializer
    mcp.ex, mcp/   the MCP server: protocol core, registry, schema, args, json, stdio
    operations/    health, telemetry
  ```

## Concepts

### Entity, aggregate, value object

```elixir
defmodule MyApp.Title do
  use DDD.ValueObject

  value_object do
    field :value, :string, required: true

    invariant MyApp.Specs.TitleWithinLimit, on: :value, max: 100
  end
end

defmodule MyApp.TodoItem do
  use DDD.Entity

  entity do
    identity :id, :uuid
    field :title, {:one, MyApp.Title}, required: true
    field :status, {:enum, [:open, :done]}, default: :open

    invariant MyApp.Specs.TitleNotBlank
  end

  defop complete(item), requires: MyApp.Specs.ItemNotCompleted do
    %{item | status: :done}
  end
end

{:ok, item} = MyApp.TodoItem.new(title: title)
{:ok, item} = MyApp.TodoItem.complete(item)
{:error, %DDD.Error{code: :precondition_failed} = error} = MyApp.TodoItem.complete(item)
DDD.Error.has_code?(error, :already_completed)   # => true
```

* `field name, type, default: ..., required: true`. Presence is `required: true`; every other
  rule is a specification attached with `invariant`. The example app has no `validate` at all.
  (`validate field, fun, message: ...` still exists as a shortcut for a throwaway one-liner,
  but it has no name, code or reuse, and it is skipped for `nil`.)
* `new/1`, `update/2` (attrs or a function of the current entity) and `validate/1` run all rules,
  and report every broken rule at once, each with its own code. Specs run after the required
  fields are present, so they only ever see a well-formed entity.
* `DDD.Aggregate` adds `emit_event/2` and `pop_events/1`. Nested entities are declared as
  `{:list, Entity}` or `{:one, Entity}` and live inside the aggregate.
* `DDD.ValueObject` has `new/1` and `equals?/2` and no `update`.
* Entities and aggregates get `created_at` and `updated_at` (`:datetime_usec`) automatically.
  `new/1` sets both, `update/2` and every `defop` bump `updated_at`, `validate/1` does not,
  and `update/2` ignores attempts to set them. Value objects are immutable and have none.

### Identity

Every entity and aggregate has an identity, made of one or more fields, one `identity` each:

```elixir
identity :id, :uuid                 # the usual case

identity :tenant_id, :uuid          # or several: the same SKU in every tenant
identity :sku, :string
```

* `Order.identity(order)` returns it as a string: the value itself for one field, the values
  joined with `:` for several (`"6f1f...:A-100"`). A `:` or `\` inside a part is escaped with a
  `\`, so two different entities can never share a string. `DDD.Identity.decode/1` splits it back.
* `Order.build_identity(tenant_id: t, sku: "A-100")` makes the same string without an entity, and
  `Order.identity_fields()` lists the part names.
* Every part is required, and none can change: `update/2` ignores them and a `defop` that changes
  one returns `:identity_changed`. A part of a composite identity must be a string, uuid, integer,
  atom, date or enum field.
* **Ids.** Only a lone `identity :id, :uuid` is generated by `new/1`, as a UUIDv7 (time-ordered, so
  indexes stay compact), and only when you leave it out. Every other identity is passed in.
  `DDD.Identity` makes ids by hand: `generate/0` (v7), `uuid7/0`, `uuid4/0`, `uuid1/0`,
  `uuid3/2`, `uuid5/2`.
* **Storage.** One field is the table's primary key in its own type (`uuid`). A composite
  identity is stored whole in a `varchar` primary key column named `identity`, next to normal
  not-null columns for each part. `find_by_id/1`, the outbox `aggregate_id` of an event, and a
  `{:ref, Aggregate}` to a composite aggregate all use the identity string.
* Changing an existing aggregate's identity is destructive: `mix vic.gen.migration` refuses
  without `--allow-destructive`, and then writes a migration with a `TODO` to backfill the new key
  for existing rows. It never drops the old key.

### Field types

Every field has a type, and a value of the wrong type is a violation with the code
`:invalid_type` (`nil` is accepted; use `required: true` to forbid it). A misspelled type fails at
compile time.

| type | value | column |
|---|---|---|
| `:string`, `:text` | binary | `varchar`, `text` |
| `:integer`, `:float`, `:boolean` | number, boolean | `bigint`, `float`, `boolean` |
| `:decimal` | `Decimal` | `numeric` |
| `:atom` | atom | `varchar` |
| `:uuid` | UUID string | `uuid` |
| `:date`, `:time`, `:naive_datetime`, `:datetime`, `:datetime_usec` | `Date`, `Time`, `NaiveDateTime`, `DateTime` | `date`, `time(6)`, `timestamp(6)`, `timestamp(0)`, `timestamp(6)` |
| `:binary` | binary | `bytea` |
| `:map`, `:json` | map, or map or list | `jsonb` |
| `{:enum, [:open, :done]}` | one of the atoms | `varchar` |
| `{:array, type}` | list of `type` | Postgres array, e.g. `varchar[]` |
| `{:map, type}` | map with string keys, values of `type` | `jsonb` |
| `{:ref, Aggregate}` | the id of another aggregate | the type of its identity |
| `{:one, Module}`, `{:list, Module}` | nested entities or value objects (a list is one JSON array) | `jsonb` |
| `:money` | `DDD.Money` (`{:one, DDD.Money}`) | `jsonb` |

    field :status, {:enum, [:open, :done]}, default: :open
    field :tags, {:array, :string}, default: []
    field :price, :money
    field :ship_to, {:one, Address}
    field :customer_id, {:ref, Customer}

`DDD.Money` is a value object with a `Decimal` amount and a 3-letter currency. `Money.add/2`,
`subtract/2` and `compare/2` return a business error (`:currency_mismatch`) for two currencies;
there is no silent conversion. A reference is a plain id, so aggregates stay separate; it is
checked against the identity type of the other aggregate. Arrays of arrays are not supported.
`DDD.Type` is the one place that knows what a type means: checking, Postgres, and JSON.

### Domain events

```elixir
defmodule MyApp.Events.ItemAdded do
  use DDD.DomainEvent

  domain_event do
    field :item_id, :string
    field :title, :string
  end
end
```

Every event has `event_id`, `occurred_at`, `created_at` and a link to the aggregate it came
from: `aggregate_type` (the module name as a string) and `aggregate_id`. `emit_event/2` fills
the link in, so events never carry the aggregate's id by hand. The outbox stores the same
link in its `aggregate_type` / `aggregate_id` columns, and the poller restores it.

### Specifications

A specification is a named, reusable, composable business rule. It is a pure function of the entity.

```elixir
defmodule MyApp.Specs.MaxOpenItems do
  use DDD.Specification, field: :items

  def check(%{items: items}, opts) do
    max = Keyword.fetch!(opts, :max)

    if Enum.count(items, &(&1.status == :open)) <= max,
      do: :ok,
      else: {:error, :too_many_open_items, "at most #{max} open items", %{max: max}}
  end
end

# attach it to an entity or aggregate:
invariant MyApp.Specs.MaxOpenItems, max: 10

# use it on its own, or compose it:
MyApp.Specs.MaxOpenItems.satisfied_by?(list, max: 10)
DDD.Specification.evaluate(DDD.Specification.all_of([A, {B, max: 3}]), entity)
```

**Value specs** check one field and are bound to it when attached, so the link is explicit and
one spec serves any field:

```elixir
defmodule MyApp.Specs.ValidEmail do
  use DDD.Specification, scope: :value

  def check(email, _opts) when is_binary(email) do
    if String.contains?(email, "@"), do: :ok, else: {:error, :invalid_email, "must be an email"}
  end
end

invariant MyApp.Specs.ValidEmail, on: :email
invariant MyApp.Specs.ValidEmail, on: :billing_email
```

`check/2` receives the value (never `nil`: absence is `required: true`'s job) and a failure is
reported under the `on:` field. Forgetting `on:` or naming a field the entity doesn't have is a
compile error, so a rename can't leave a spec silently checking nothing. A spec that needs
several fields (`use DDD.Specification, field: :items`, `check(entity, opts)`) is an entity spec
and takes no `on:`.

`all_of/1`, `any_of/1` and `not_spec/1` build composite specs. Specs run only after field rules
pass, so they can assume required fields are present. Rules that need the database (for
example "the owner exists") belong in the command handler.

### Business operations: `defop`

`defop` marks a function as a business operation. The body returns the new struct, or
`{:error, reason}`. The framework then validates and seals the result.

**State the business rules explicitly, as named preconditions**, not by pattern-matching the
entity in the function head:

```elixir
defop complete(item), requires: MyApp.Specs.ItemNotCompleted do
  %{item | status: :done}
end

defop rename(item, %MyApp.Title{} = title), requires: MyApp.Specs.ItemEditable do
  %{item | title: title}
end

# Several rules, some with options; all of them are checked and all failures reported:
defop ship(order), requires: [MyApp.Specs.OrderIsPaid, {MyApp.Specs.InStock, warehouse: :main}] do
  %{order | status: :shipped}
end
```

`requires:` takes specifications (see above), checked against the entity (the first argument)
before the body runs. A broken rule returns a business error with `code: :precondition_failed`,
the rule's own message, and one violation per broken rule carrying its code:
`DDD.Error.has_code?(error, :already_completed)`. Every broken rule is reported at once.
Invariants are the mirror image: they check the state *after* an operation.

The operation's *other* arguments reach each spec as `opts[:args]`, so a rule can depend on
them. That is how the example's `TodoList.complete_item(list, item_id)` states its prerequisites
where a reader looks:

```elixir
defop complete_item(list, item_id),
  requires: [Specs.ItemExists, {Specs.ItemSatisfies, spec: Specs.ItemNotCompleted}] do
  ...
end

# ItemExists reads the id from opts[:args]:
def check(%{items: items}, opts) do
  [item_id | _] = Keyword.fetch!(opts, :args)
  if Enum.any?(items, &(&1.id == item_id)), do: :ok, else: {:error, :item_not_found, "..."}
end
```

`ItemSatisfies` lifts a rule about one item (written once, on the item) to the list that holds
it. Specs are evaluated against the first argument only, so a rule about an item can't be
attached to the list directly: the list has no `status`, and the spec would pass every call.

Patterns in the head are for the *shape of the arguments* (`%Date{}`, `%Title{}`), not for
business rules.

* Arguments that match no clause: `{:error, %DDD.Error{code: :invalid_operation}}`.
* Events emitted with `emit_event/2` are dropped when the operation is rejected or the result
  is invalid.
* An operation with several clauses is guarded by the preconditions of all of them.

### Errors

Every failure is `{:error, %DDD.Error{}}` with `kind`, `entity`, `entity_id`, `operation`, `code`,
`message`, `details`, `violations` and `context`. `entity` is the kind of entity (the module) and
`entity_id` the identity of the instance it happened to (`Entity.identity/1`, so
`"tenant:sku"` for a composite identity), or `nil` when there is none: a `new` call missing its
identity, or an error that isn't about an instance. Validation, preconditions, unsealed structs,
identity changes, a lost update and a failed save all carry it. An MCP client sees `entity` (the
short name) and `entity_id` in a business error. The `kind` is `:business` (the domain said no;
retrying gives the same answer) or `:infrastructure` (the system failed; retrying may succeed).
Running a command adds `command` to `context`. `DDD.Error.has_code?/2` and
`DDD.Error.to_map/1` (a `%{field => [messages]}` map) help callers.

### Sealing

Every struct has a required `__sealed__` key, so `%Todo{...}` literals do not compile by
accident. The seal is a fingerprint of the data, set only after validation. Repositories and
`defop` reject structs whose seal does not match (`code: :unsealed`). `defop` checks every framework
struct among its arguments, not just the entity, so `add_item(list, item)` refuses a forged `item`.

An aggregate's **pending events** are sealed too. `emit_event/2` and `pop_events/1` are the only
ways to change them and they keep the seal, so replacing the list by hand makes the aggregate
unsealed and `defop`, `update/2` and `save/1` refuse it. `validate/1` also refuses an unsealed
aggregate that carries events, rather than sealing events it cannot vouch for.

### Repositories

```elixir
defmodule MyApp.TodoListRepository do          # domain layer: the port
  use DDD.Repository
  repo MyApp.TodoList do
    find_by :owner_id
  end
end

defmodule MyApp.Infrastructure.TodoListRepository do   # infrastructure: generated adapter
  use DDD.Repository.Ecto, port: MyApp.TodoListRepository, repo: MyApp.Repo, table: "todo_lists"
end
```

The whole aggregate is one row. Nested entities are stored as `jsonb` and rebuilt into sealed
structs on load; keys missing from old rows take the field default. `save/1` writes the
aggregate and its domain events (the outbox) in one transaction.
`DDD.InMemoryRepository` is available for tests.

### Optimistic locking

Every aggregate has a `version`, which starts at 0 and is advanced by the repository on every
save, never by the domain (`new/1` and `update/2` ignore it; a `version` field you declare
yourself is left alone). `save/1` writes `UPDATE ... WHERE id = ? AND version = ?`, so two
requests that loaded the same aggregate cannot both win: the second one gets

```elixir
{:error, %DDD.Error{kind: :infrastructure, code: :stale_aggregate,
                    details: %{id: ..., expected_version: 3, actual_version: 4}}}
```

and nothing is written, events included. It is an *infrastructure* error: nothing about the
request is wrong, and running the command again on fresh data may well succeed. The outbox
retries a handler that fails this way by itself; for a command say how often to re-run it:

```elixir
use DDD.Command, retry_on_conflict: 3      # re-runs execute/1, up to 3 more times
```

A save is also a conflict when the row was deleted since you loaded it, or when a *new*
aggregate (version 0) uses an id that already exists. A command called inside an open
transaction is not retried: that transaction is already doomed. `mix vic.gen.migration` adds the
column (`null: false, default: 0`, so existing rows start at 0). Each conflict is a
`[:ddd, :repository, :conflict]` telemetry event.

### Listing: filters, pages and cursors

An Ecto adapter (and a projection) also has `list/1`, `count/1`, `page/1` and `slice/1`:

```elixir
TodoListRepository.list(filter: [owner_id: id, status: {:in, [:open, :late]}], order: :title)

{:ok, %DDD.Page{entries: lists, page: 2, total_entries: 57, total_pages: 3}} =
  TodoListRepository.page(filter: [owner_id: id], order: [desc: :created_at], page: 2, page_size: 20)

# keyset pagination: no OFFSET, no totals, stable while rows come and go
{:ok, %DDD.Slice{entries: lists, next_cursor: cursor}} = Repo.slice(order: :title, limit: 20)
{:ok, next} = Repo.slice(order: :title, limit: 20, after: cursor)
```

The filter language (`DDD.Filter`) has equality, `:gt :gte :lt :lte :neq :in :not_in :like
:ilike`, `nil` for `NULL`, and `{:and, _}`, `{:or, _}`, `{:not, _}`. Values are bound parameters.
The identity is always the last sort key, so every order is total and pages never overlap. A bad
field, operator, order, page number or cursor is a *business* error (`:invalid_filter`,
`:invalid_order`, `:invalid_page`, `:invalid_cursor`), because these usually arrive straight from
a request.

**Specifications can search.** A spec is a pure function of an entity, so the database can't run
it; a spec that also defines `to_filter/1` (the same rule, written as a filter) can be given to
the repository, alone or composed with `all_of`, `any_of` and `not_spec`:

```elixir
def to_filter(_opts), do: [status: :open, due: {:lt, Date.utc_today()}]

TodoListRepository.list(spec: DDD.Specification.all_of([Overdue, {OwnedBy, owner: id}]))
```

A spec without `to_filter/1` raises an `ArgumentError` there: it can only be checked in memory.
Keep the two in step; a test that runs both over the same rows is cheap.

### Commands

A command is the input of one use case and carries its own handler, `execute/1`. The handler
loads aggregates through repositories, calls domain operations and saves. It holds no business
rules.

```elixir
defmodule MyApp.Commands.AddItem do
  use DDD.Command

  command do
    field :list_id, :string, required: true
    field :title, :string, required: true
  end

  @impl DDD.Command
  def execute(%__MODULE__{list_id: list_id, title: title}) do
    with {:ok, list} <- load_list(list_id),
         {:ok, item} <- MyApp.TodoItem.new(title: title),
         {:ok, list} <- MyApp.TodoList.add_item(list, item) do
      MyApp.Infrastructure.TodoListRepository.save(list)
    end
  end
end

MyApp.Commands.AddItem.call(%{list_id: id, title: "Buy milk"})   # builds, validates, executes
```

* `new/1` builds and validates the command (`required: true` fields).
* `call/1` takes a struct or plain attrs. Every error comes back as a `DDD.Error` with
  `context: %{command: MyApp.Commands.AddItem}`. `call!/1` raises instead.

**Transactions.** `execute/1` runs in its own database transaction by default. `{:ok, _}`
commits; `{:error, _}` rolls back everything the handler did, including every aggregate it
saved and their outbox events; an exception rolls back and propagates. Repository
transactions inside the handler join it, so a command that saves two aggregates is atomic.
Validation in `new/1` runs before the transaction starts.

```elixir
use DDD.Command                        # default: transaction on the configured repo
use DDD.Command, repo: MyApp.Repo      # another repo
use DDD.Command, transaction: false    # no transaction (e.g. in-memory repositories)
```

### Queries

A `DDD.Query` is the read counterpart of a command: fields, `new/1`, `call/1`, `execute/1`, but it
never saves an aggregate or emits an event. Its `execute/1` runs in a **read-only Postgres
transaction**, so "this cannot change data" is enforced by the database, and it sees one
consistent snapshot. `use DDD.Query, transaction: false` opts out; a query called inside an open
transaction joins it.

```elixir
defmodule MyApp.Queries.OpenItems do
  use DDD.Query

  query do
    description "The items of a list that are not done yet"
    field :list_id, :uuid, required: true, description: "The list to read"
  end

  @impl DDD.Query
  def execute(%__MODULE__{list_id: id}), do: ...    # {:ok, data} | {:error, reason}
end
```

Command and query fields are `DDD.Type`s, checked on `new/1` (`:invalid_type`). Every module
that can be described takes `description "..."`: commands, queries, entities, aggregates and
value objects; `field ..., description: "..."` describes a field and
`defop name(...), description: "..."` an operation. Descriptions are plain metadata
(`__description__/0`, `__ddd_description__/0`); the MCP server reads them.

### MCP server

The framework can serve the application over the [Model Context Protocol](https://modelcontextprotocol.io),
so an AI client such as Claude can call its commands and queries and read its aggregates. The
protocol core is `DDD.MCP` (JSON-RPC 2.0, no extra dependency); the first transport is stdio.

```elixir
defmodule MyApp.Interface.MCP do
  use DDD.MCP.Server,
    instructions: "Manage todo lists.",
    commands: [MyApp.Commands.CreateList, {MyApp.Commands.RenameItem, name: "rename_todo"}],
    queries: [MyApp.Queries.OpenItems],
    repos: [MyApp.TodoList.Repository],
    read_only: false,
    authorize: &MyApp.MCPPolicy.authorize/2
end
```

```
mix compile && mix vic.mcp MyApp.Interface.MCP            # serve on stdin/stdout
mix vic.mcp MyApp.Interface.MCP --read-only               # queries and resources only
claude mcp add todo -- mix vic.mcp MyApp.Interface.MCP
```

* **Nothing is exposed unless it is listed**, and the commands and entities carry no MCP code.
  What you list is the level of access: only `queries:` and `repos:` is a read-only server.
* **Tools.** Each command or query is a tool named after the module (`AddItem` is `add_item`,
  overridable per entry; two with one name stop the server at startup). Its input schema is built
  from the fields: the `DDD.Type` (enums become `enum`, `:uuid` a UUID string, nested value
  objects objects), `required`, `default` and the descriptions. Queries carry `readOnlyHint: true`.
  A module with no `description` cannot be exposed. Arguments are converted from JSON through
  `DDD.Type`; nested entities and value objects are built with their own `new/1`, so their rules
  run, and atoms are never created from input.
* **Resources.** An exposed aggregate (list its repository port, which must define `impl/0`) is
  read-only: `todo_list://<identity>` reads one and `todo_list://by/owner_id/<value>` runs a
  `find_by` the port declares, at most 100 results (`truncated` says when it was cut).
  `resources/list` is empty on purpose: nothing hands a client a whole table. Every field of the
  aggregate is shown; to keep data back, don't expose the aggregate and return a curated map from
  a query.
* **Errors.** A business error (validation, not found, a precondition) is returned with its code,
  message and violations so the client can correct itself. An infrastructure error is a generic
  "the operation failed" and its details only go to the log. A malformed call gets a JSON-RPC
  error saying what was wrong.
* **Access.** `read_only: true` (or `--read-only`) refuses every command. `authorize:` is called
  with `(tool_name, arguments)` before each tool call, and with `("resource:<name>", %{"uri" => uri})`
  before each resource read; return `:ok` or `{:error, reason}`.
* **Stdio hygiene.** Standard output carries only protocol messages. The task moves the logger to
  standard error and turns the SQL log and the outbox poller off. Run `mix compile` first, because
  Mix prints its compile output on standard output when sources changed.

## Migrations

```bash
mix vic.gen.outbox_migration                 # the domain_events outbox table
mix vic.gen.migration MyApp.TodoList         # first run: create table
mix vic.gen.migration MyApp.TodoList         # later runs: only the difference
```

The generator keeps a snapshot of each aggregate in `priv/ddd_snapshots/` and writes the
difference as a new migration. It never overwrites earlier migrations.

* Removing a column or changing its type loses data, so it is refused unless you pass
  `--allow-destructive`. Destructive operations are marked in the generated file.
* Renames must be explicit, otherwise they are a remove plus an add:
  `--rename old:new`, or `field :name, :string, renamed_from: :old_name`. (The example app moved
  from `inserted_at` to `created_at` this way, with no data loss.)
* Edits inside a nested entity do not change columns. Old rows keep the old shape, and
  defaults fill the missing keys on load. Reshaping existing data needs a hand-written migration.
* Nested lists are stored as one `jsonb` array.

Check for drift between aggregates and the real tables in a test:
`DDD.Migration.drift(MyApp.TodoList, "todo_lists", MyApp.Repo)`.

## Outbox

Events are written to the `domain_events` table in the same transaction as the aggregate (and
inside the command's transaction), so a state change and its events commit together or not at
all. `DDD.Outbox.Poller` then delivers them to their handlers:

1. **Claim** a batch of due events with a lease and `FOR UPDATE SKIP LOCKED`, so several
   pollers or nodes never take the same event.
2. **Deliver** to every subscribed handler, synchronously.
3. **Mark published only when every handler succeeded or rejected.** A system failure is
   retried after an exponential backoff (1s, 2s, 4s ... capped at 5 minutes). After
   `max_retries` retries (default 10, so 11 attempts) the event is dead-lettered: `failed_at`
   is set, `last_error` says why, and it is no longer claimed.

The attempt is counted when the event is claimed, so even an attempt that kills the whole VM
uses one up. If the server stops mid-delivery, the lease expires and the event is delivered
again; handlers that had already succeeded are skipped.

### Handlers

```elixir
defmodule MyApp.OnItemCompleted do
  use DDD.EventHandler, events: [MyApp.Events.ItemCompleted]

  @impl DDD.EventHandler
  def handle(%MyApp.Events.ItemCompleted{aggregate_id: list_id}) do
    case MyApp.Commands.RecordProgress.call(%{list_id: list_id}) do
      {:ok, _} -> :ok
      {:error, error} -> {:error, error}   # business: rejected. infrastructure: retried
    end
  end
end

MyApp.OnItemCompleted.subscribe()
```

What `handle/1` returns decides what happens, using the kind of the error (`DDD.Error.kind`):

| `handle/1` returns | Meaning | Outcome |
|---|---|---|
| `:ok` | handled | delivered |
| `{:error, %DDD.Error{kind: :business}}` or a plain reason like `:not_found` | the handler looked at the event and said no. Deterministic. | **rejected**: recorded in `event_deliveries` (`outcome: "rejected"`), never retried, **not** a dead letter |
| `{:error, %DDD.Error{kind: :infrastructure}}`, an exception, a throw | the system failed | **retried** with backoff; dead-lettered when retries run out |

In both failure cases a transactional handler is rolled back first. A rejection is remembered,
so the handler is not run again for that event.

Errors the framework creates carry the right kind: validation, invalid operations and
unsealed structs are `:business`; a rolled-back command transaction, a failed save and
exceptions are `:infrastructure`. Build your own with `DDD.Error.business/1` and
`DDD.Error.infrastructure/1`. A plain reason is the domain's answer, so it counts as business.

**Each delivery runs in its own transaction by default**, together with a row in
`event_deliveries` that records "this handler processed this event". If the handler fails,
everything rolls back, including that row, and the event is retried. If it succeeds, the handler
is never run again for that event, even if another handler of the same event failed. For
handlers that only write to our database this is effectively exactly-once. Events a handler
produces (by saving aggregates) commit with its work, so chained events are safe too: they only
exist if the handler succeeded.

For an effect a transaction can't undo (sending an email, calling an API), use
`transaction: false`. That handler is delivered at least once and must tolerate a repeat:

```elixir
use DDD.EventHandler, events: [...], transaction: false
use DDD.EventHandler, events: [...], repo: MyApp.Repo
```

Plain modules with `handle/1` work as well and are delivered like `transaction: false`.

### Pruning

`domain_events` and `event_deliveries` grow with every event. Delete the old ones when you
don't need them any more:

```bash
mix vic.outbox.prune --older-than-days 30
```

or on a timer, with `{DDD.Supervisor, retention: [older_than_days: 30]}`, or from your own
scheduler with `DDD.Outbox.Retention.prune(MyApp.Repo, older_than_days: 30)`. Only published
events and resolved dead letters are deleted; waiting events, events being retried and unresolved
dead letters are kept. Nothing is pruned unless you ask: there is no default age. A projection
is rebuilt by replaying the events that are left (see *Projections*), so prune after the
history you want to be able to rebuild from, or archive the table first.

### Retry configuration

```elixir
config :vicander, :outbox,
  max_retries: 10,           # retries after the first attempt (default 10)
  backoff_base_ms: 1_000,    # the first retry waits this long, then it doubles (default 1s)
  backoff_max_ms: 300_000    # never wait longer than this (default 5 minutes)
```

A poller option wins over the application config, which wins over the default:
`{DDD.Outbox.Poller, repo: MyApp.Repo, max_retries: 3, backoff_ms: fn retry -> ms end}`.

### Dead letters

An event is dead-lettered only when the system kept failing: infrastructure errors,
exceptions, or an event type that can't be resolved. Business rejections are never dead
letters. When the poller gives up it logs an error, emits a `[:ddd, :outbox, :dead_letter]`
telemetry event (attach a handler to alert), and notifies `DDD.Outbox.DeadLetterHandler`, a
supervised process that applies a policy:

| Policy returns | Effect |
|---|---|
| `:keep` | acknowledge it; it stays dead-lettered for a person to deal with |
| `:replay` | clear the failure state; the poller retries it (handlers that succeeded are skipped) |
| `:discard` | resolve it as discarded |
| `:later` | decide on a later sweep |

The **default policy keeps** every dead letter: the failure is logged and emitted as telemetry,
and a person replays it. Replaying automatically before anyone has looked mostly repeats the
failure, so it is opt-in, for transient causes such as an outage longer than the retries:

```elixir
config :vicander, :dead_letters,
  max_replays: 3,             # default 0: never replay automatically
  replay_cooldown_ms: 60_000, # wait this long after it died
  policy: MyApp.DeadLetterPolicy   # or your own `DDD.Outbox.DeadLetterPolicy`
```

The database is the source of truth. The handler sweeps for unresolved dead letters when it
starts and every minute, so a crash and restart (or a lost notification) loses nothing. A
policy that raises is logged and treated as `:later`; it cannot crash the handler.

By hand:

```bash
mix vic.outbox.dead_letters         # unresolved ones (--all includes resolved)
mix vic.outbox.replay EVENT_ID      # or --all; fix the cause first
mix vic.outbox.discard EVENT_ID
```

### Setup

`{DDD.Supervisor, handlers: [...]}` (see *Setup* at the top) starts the poller and the
dead-letter handler. Handlers are subscribed first, so an event is never delivered to an empty
list. `mix vic.gen.outbox_migration` creates the `domain_events` and `event_deliveries` tables.

Limits: handler cycles (A causes B causes A) are not detected, and events are ordered per poll
batch, not across a whole chain. Two handlers saving the same aggregate at the same moment no
longer overwrite each other (see *Optimistic locking*): the loser fails with `:stale_aggregate`
and is retried.

## Projections

A read model is a denormalized table kept up to date by events, so a screen that needs "my lists
with their counts" reads one table instead of loading and folding every aggregate.
`DDD.Projection` is an event handler (see *Handlers*), so it is delivered through the outbox and
inherits its guarantees: every event is applied in its own transaction together with its delivery
record, so a retry or a duplicate delivery never counts an event twice.

```elixir
defmodule MyApp.Projections.ListSummary do
  alias MyApp.Events.{ListCreated, ItemAdded, ItemCompleted}

  use DDD.Projection, table: "list_summaries", key: :list_id,
    events: [ListCreated, ItemAdded, ItemCompleted]

  projection do
    description "One row per list: title and counts"
    field :list_id, :uuid
    field :title, :string
    field :open_count, :integer, default: 0
    field :done_count, :integer, default: 0
  end

  on %ListCreated{} = e, do: {:upsert, %{title: e.title}}
  on %ItemAdded{}, do: {:inc, :open_count, 1}
  on %ItemCompleted{}, do: {:inc, open_count: -1, done_count: 1}
end

MyApp.Projections.ListSummary.subscribe()     # before the poller starts
```

The row is keyed by the event's `aggregate_id` (`key_from:` picks another event field). An `on`
clause returns what to do to that row: `{:upsert, map}` (create with the field defaults, or set
these fields), `{:update, map}` (only if the row exists), `{:inc, field, n}` (the database adds,
so concurrent events never overwrite each other), `:delete`, `:skip`, a list of these, or
`{:error, reason}` (a rejection, like in any handler). Events can arrive before the row exists:
an increment creates it.

* **Table.** `mix vic.gen.migration MyApp.Projections.ListSummary` writes the migration.
* **Reading.** `get/1`, and `list/1`, `count/1`, `page/1`, `slice/1` with the filters, orders and
  cursors above. Rows are plain maps.
* **Consistency.** Eventually consistent: it trails by the time the outbox takes. Decisions read
  the aggregate, never a projection.
* **Lag.** `lag/0` returns `%{pending: n, oldest_pending_age_ms: ms}`: the events of its types
  that no delivery record covers yet (discarded dead letters excluded).
* **Rebuild.** `mix vic.projection.rebuild MyApp.Projections.ListSummary` (or `rebuild/0`)
  empties the table and replays every stored event of its types, in order. Use it after fixing an
  `on` clause, or to build a new projection from history. The events must still be in
  `domain_events`. Readers see the table fill up again while it runs, so do it in a quiet period;
  if it fails part-way the error says the projection is partial, and running it again is safe.

The example app has one: `TodoApp.Application.Projections.TodoList.ListStats` (items added and completed per
list). It lives in the application layer, beside the query that reads it (`list_activity`): a
projection is an event handler that maintains a read model, and the framework hides the SQL, so
nothing in it is specific to an adapter. Only the repositories, which the domain declares as
ports, sit behind `impl/0`.

## Testing your application

`DDD.Test` has the assertions an application's tests keep writing:

```elixir
import DDD.Test

{:ok, list} = TodoList.complete_item(list, item.id)
assert_event_emitted(list, ItemCompleted, item_id: item.id)   # returns the event
refute_event_emitted(list, ListReopened)

assert_error_code(TodoList.complete_item(list, item.id), :already_completed)   # returns the error

{:ok, _} = Commands.AddItem.call(...)
drain_outbox()      # delivers the saved events to the subscribed handlers, now
```

In tests, start the framework with a poller that doesn't run by itself
(`{DDD.Supervisor, handlers: [...], poller: [interval_ms: 600_000]}`) and call `drain_outbox/0`
when a test needs the handlers to have run, or pass `poller: false` and test the handlers
directly. `DDD.InMemoryRepository` replaces a Postgres adapter in tests that don't need a
database. The example app's `TodoApp.DataCase` shows the Ecto sandbox setup.

## Observability

Every framework event is a `:telemetry` event; `DDD.Telemetry` has the full catalogue.

| what | event |
|---|---|
| a command or query | `[:ddd, :command]` / `[:ddd, :query]` spans, with `:result`, `:error_codes`, `:error_kind` |
| a command re-run after a version conflict | `[:ddd, :command, :retry]` |
| a lost optimistic-lock race | `[:ddd, :repository, :conflict]` |
| a handler delivery | `[:ddd, :handler, :deliver]` span, with `:outcome` (`:delivered`, `:skipped`, `:rejected`, `:error`) |
| the outbox | `[:ddd, :outbox, :poll \| :published \| :retry \| :dead_letter]` |
| a projection rebuild | `[:ddd, :projection, :rebuild]` span |
| outbox and projection gauges | `[:ddd, :outbox, :stats]`, `[:ddd, :projection, :lag]` |

The gauges are read from the database, so emit them on a timer (for example with
`:telemetry_poller`): `DDD.Telemetry.report(MyApp.Repo, [MyApp.Projections.ListSummary])`. A
stuck outbox shows up as a growing `oldest_pending_age_ms`, and `dead_letters` above zero means
someone has to act (`mix vic.outbox.dead_letters`).

**Health.** `DDD.Health.check/1` answers `:ok`, `:degraded` (unresolved dead letters, an event or a
projection waiting longer than its threshold) or `:unhealthy` (the database does not answer):

```elixir
config :vicander, :health,
  projections: [MyApp.Projections.ListSummary],
  max_outbox_age_ms: 60_000, max_dead_letters: 0, max_projection_lag_ms: 60_000
```

The result is JSON-encodable: serve it from a Plug route or a Phoenix controller, or list
`DDD.Health.Query` in an MCP server's `queries:`. `mix vic.health [--json]` prints it and exits
non-zero unless `:ok`, for a deploy gate or a container probe.

## Pending work

Known gaps, in the order they matter. None is hidden: each was found and decided on.

* **Concurrent first saves.** Optimistic locking guards every save of an existing aggregate, and
  a new aggregate whose id already exists is a `:stale_aggregate` conflict. Two *simultaneous*
  first saves of the same new id can still both pass that check, and the second insert then fails
  with a raw constraint error instead of a `DDD.Error`. Ids are UUIDv7, so this only happens when
  the same new struct is saved twice at once.
* **Projection ordering.** A projection applies events in delivery order, which follows
  `occurred_at` within a poll batch. Its `on` clauses should be commutative (increments, upserts
  of independent fields) rather than depend on strict order; events carry no aggregate version
  yet that a projection could use to detect an out-of-order one.
* **Outbox lease per batch.** A claimed batch of up to 100 events shares one 30-second lease.
  With slow handlers the unprocessed tail can be claimed again by another node. Renew the lease
  per event, or claim fewer events.
* **Logged parameters.** `DDD.Command` and `DDD.Query` log every parameter value at info level.
  There is no way to keep a password or token out of the log yet.


## Example application

`todo_app` (a sibling project, `../todo_app`) is a complete application on top of the framework: four layers
(domain, application, infrastructure, interface) with the layering enforced by
[`boundary`](https://hex.pm/packages/boundary), aggregates with specifications, commands and
queries (paged and cursor listings), a read model, event handlers, and an MCP server. It is the
first thing to read to see how the pieces fit, and its test suite is the framework's integration
test.

```elixir
alias TodoApp.Application.{Commands, Queries}

{:ok, user} = Commands.User.RegisterUser.call(%{name: "Ann", email: "ann@example.com"})
{:ok, list} = Commands.TodoList.CreateList.call(%{owner_id: user.id, name: "Home"})
{:ok, list} = Commands.TodoList.AddItem.call(%{list_id: list.id, title: "Buy milk"})
{:ok, open} = Queries.TodoList.OpenItems.call(%{list_id: list.id})
```
