A command is the input of one use case, and it carries its own handler.
The command declares its fields (validated on construction) and implements execute/1:
load aggregates through repositories, call domain operations, save. It holds no business
logic itself; the rules live in entities, aggregates and specifications.
Usage
defmodule MyApp.Commands.PlaceOrder do
use DDD.Command
command do
field :customer_id, :string, required: true
field :items, :list, required: true
field :notes, :string, default: nil
end
@impl DDD.Command
def execute(%__MODULE__{customer_id: customer_id, items: items}) do
with {:ok, order} <- MyApp.Order.place(%{customer_id: customer_id}),
{:ok, order} <- MyApp.Order.add_items(order, items) do
MyApp.Infrastructure.OrderRepository.save(order)
end
end
end
MyApp.Commands.PlaceOrder.call(%{customer_id: "c1", items: [...]})Running a command
new/1builds and validates the command:{:ok, command}or{:error, %DDD.Error{}}.call/1takes a command struct or plain attrs (built withnew/1first), runsexecute/1, and returns its result. Any error comes back as aDDD.Errorwhosecontexthas:commandset to the command module.call!/1iscall/1that raises on failure.
Transactions
execute/1 runs in its own database transaction by default: {:ok, _} commits,
{:error, _} rolls back everything the handler did (every aggregate it saved, and their
outbox events), and an exception rolls back and propagates. The repositories' own
transactions join it.
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)Validation in new/1 happens before the transaction starts.
Retrying version conflicts
Aggregates are saved with optimistic locking: a save that lost against a concurrent one fails
with :stale_aggregate (an infrastructure error). retry_on_conflict: n re-runs the whole
command, on freshly loaded data, up to n more times before giving up:
use DDD.Command, retry_on_conflict: 3A command called from inside an open transaction is never retried (the outer transaction is already doomed), and neither is any other error.
Telemetry
Every run is a [:ddd, :command] span; see DDD.Telemetry.