원클릭으로
commanded-expert
Commanded CQRS/event-sourcing patterns for aggregates, commands, events, process managers, projections, and event handlers
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Commanded CQRS/event-sourcing patterns for aggregates, commands, events, process managers, projections, and event handlers
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Python CLI patterns — typer apps, uv with PEP 723 inline deps, pytest, pathlib, atomic file writes, file locking, JSONL streaming, and production-grade stdlib idioms for command-line tools
Phoenix LiveView patterns for stateful server-rendered UI including streams, async assigns, components, forms, uploads, and JS hooks
Oban background job patterns — workers, queues, scheduling, unique jobs, retries, telemetry, engines, and Oban Pro features (Workflow, Batch)
Elixir testing patterns — ExUnit organization, ExMachina factories, Mox boundary mocking, Bypass HTTP stubbing, parameterize, sandbox patterns, and async-safe concurrency
Ecto database patterns — schemas, changesets, query composition, Multi transactions, migrations, ParameterizedType, UUIDv7, and performance tuning
Absinthe GraphQL patterns — schema design, resolvers, middleware, Dataloader batching, subscriptions, complexity analysis, and file uploads
| name | commanded-expert |
| description | Commanded CQRS/event-sourcing patterns for aggregates, commands, events, process managers, projections, and event handlers |
| targets | {"elixir":"1.18+","commanded":"1.4+","otp":"27+"} |
ecto-expert)execute/2 decides what events to emit; apply/2 rebuilds state from events. No I/O inside either.Command Dispatch:
Caller → Router → Aggregate.execute(state, command)
↓ returns events (or {:error, reason})
Event Store (append to stream)
↓
Subscribers (async unless :strong consistency)
├── Projections → Read models (Ecto)
├── Process Managers → Dispatch new commands
└── Event Handlers → Side effects (email, webhook)
Aggregate state on cold load:
Event Store → replay events → Aggregate.apply(state, event) for each → current state
(or load snapshot + replay tail events)
What kind of behavior?
├── Validates intent + decides what happened? → Aggregate (execute/2)
├── Updates aggregate state from a fact? → Aggregate (apply/2)
├── Reacts to events with new commands across aggregates? → Process Manager
├── Builds a queryable read model? → Projection (Commanded.Projections.Ecto)
├── Triggers a side effect (email, webhook, notification)? → Event Handler
├── Runs on a schedule, not in response to an event? → Oban worker (outside Commanded — see `oban-expert`)
└── Cross-cutting concern (logging, metrics)? → Commanded middleware
Where does this multi-step workflow live?
├── All within one aggregate's invariants? → Aggregate (execute returns multiple events)
├── Spans multiple aggregates / streams? → Process Manager
│ ├── Linear flow (A done → do B → do C)? → Process Manager with state machine
│ └── Saga with compensation (rollback on failure)? → Process Manager + compensating commands
├── Driven by external trigger (cron, webhook)? → Oban or external dispatcher → command
└── One event, one side effect, no new commands? → Event Handler (not Process Manager)
What does the caller need after dispatch returns?
├── Just acknowledgement that command was accepted? → :eventual (default, fastest)
├── Read model must reflect the change before responding (e.g., redirect after create)? → :strong
│ └── Cost: dispatch blocks until ALL strong handlers complete
├── Specific projection updated, others can lag? → :strong with explicit handler list
│ └── dispatch(cmd, consistency: [MyApp.Projections.UserList])
└── Fire-and-forget background work? → :eventual + GenServer/Oban for retry
How long does aggregate replay take on cold load?
├── < 100 events per stream? → No snapshots needed
├── 100-1,000 events, occasional load? → No snapshots needed (replay is cheap)
├── 1,000-10,000 events, frequent load? → Enable snapshots (every N events)
├── Long-lived aggregates accumulating events? → Snapshots + consider stream archival
└── Aggregate state shape changing? → Bump @snapshot_version, will rebuild from events
defmodule MyApp.Accounts.Commands.RegisterUser do
@enforce_keys [:user_id, :email, :name]
defstruct [:user_id, :email, :name]
use Vex.Struct
validates :user_id, uuid: true
validates :email, presence: true, format: ~r/@/
validates :name, presence: true, length: [min: 1]
end
Rule: Commands are plain structs with validation. The user_id (or aggregate_id) is required so the router can dispatch to the right stream.
defmodule MyApp.Accounts.Events.UserRegistered do
@derive Jason.Encoder
defstruct [:user_id, :email, :name, :registered_at]
end
Rule: Events are immutable. Once an event is in the store, you can never rename a field or change semantics — only add new fields (with default nil) or write a new event version + upcaster.
defmodule MyApp.Accounts.User do
@snapshot_version 1 # bump whenever the struct shape below changes
defstruct [:user_id, :email, :name, :status]
alias MyApp.Accounts.Commands.{RegisterUser, UpdateEmail}
alias MyApp.Accounts.Events.{UserRegistered, EmailUpdated}
# execute/2 — given current state and a command, return events (or error)
def execute(%__MODULE__{user_id: nil}, %RegisterUser{} = cmd) do
%UserRegistered{
user_id: cmd.user_id,
email: cmd.email,
name: cmd.name,
registered_at: DateTime.utc_now()
}
end
def execute(%__MODULE__{}, %RegisterUser{}), do: {:error, :already_registered}
def execute(%__MODULE__{user_id: nil}, _), do: {:error, :user_not_registered}
def execute(%__MODULE__{email: same}, %UpdateEmail{email: same}), do: [] # no-op
def execute(%__MODULE__{}, %UpdateEmail{} = cmd) do
%EmailUpdated{user_id: cmd.user_id, email: cmd.email, updated_at: DateTime.utc_now()}
end
# apply/2 — given current state and an event, return new state
def apply(%__MODULE__{} = state, %UserRegistered{} = ev) do
%__MODULE__{state | user_id: ev.user_id, email: ev.email, name: ev.name, status: :active}
end
def apply(%__MODULE__{} = state, %EmailUpdated{email: email}) do
%__MODULE__{state | email: email}
end
end
Rule: execute/2 and apply/2 must be pure — no Repo, no HTTPoison, no external reads. Exception: timestamps (DateTime.utc_now()) and UUIDs may be generated inside execute/2 to stamp a value into the event itself. The event then carries that timestamp forever — replaying the event uses the original value, preserving determinism. Never read external state to decide what events to emit; the aggregate struct is the only input.
defmodule MyApp.Router do
use Commanded.Commands.Router
alias MyApp.Accounts.User
alias MyApp.Accounts.Commands.{RegisterUser, UpdateEmail}
identify(User, by: :user_id, prefix: "user-")
dispatch([RegisterUser, UpdateEmail],
to: User,
lifespan: MyApp.Accounts.UserLifespan
)
end
defmodule MyApp.Onboarding.WelcomeProcess do
use Commanded.ProcessManagers.ProcessManager,
application: MyApp.CommandedApp,
name: __MODULE__
defstruct [:user_id, :status]
alias MyApp.Accounts.Events.UserRegistered
alias MyApp.Notifications.Commands.SendWelcomeEmail
# interested?/1 — what events start or continue this process?
def interested?(%UserRegistered{user_id: id}), do: {:start, id}
# handle/2 — given state and event, return commands to dispatch
def handle(%__MODULE__{}, %UserRegistered{user_id: id, email: email, name: name}) do
%SendWelcomeEmail{user_id: id, email: email, name: name}
end
# apply/2 — track process state if you need it across events
def apply(%__MODULE__{} = state, %UserRegistered{user_id: id}) do
%__MODULE__{state | user_id: id, status: :welcomed}
end
# Tear down the process manager when the workflow completes.
# Without this, state persists indefinitely.
def apply(%__MODULE__{} = state, %WelcomeEmailSent{}) do
{:stop, %__MODULE__{state | status: :completed}}
end
end
Rule: Process Manager state persists across restarts — it's stored in the event store like any other stream. Return {:stop, new_state} from apply/2 when the workflow completes, otherwise zombie process managers accumulate forever.
defmodule MyApp.Accounts.Projections.UserProjection do
use Commanded.Projections.Ecto,
application: MyApp.CommandedApp,
repo: MyApp.Repo,
name: "Accounts.UserProjection",
consistency: :eventual
alias MyApp.Accounts.Events.{UserRegistered, EmailUpdated}
alias MyApp.Accounts.ReadModels.User
project(%UserRegistered{} = ev, _meta, fn multi ->
Ecto.Multi.insert(multi, :user, %User{
id: ev.user_id,
email: ev.email,
name: ev.name,
registered_at: ev.registered_at
})
end)
project(%EmailUpdated{user_id: id, email: email}, _meta, fn multi ->
Ecto.Multi.update_all(multi, :user, from(u in User, where: u.id == ^id), set: [email: email])
end)
end
Rule: Projections use Ecto.Multi so the projection write and Commanded's tracking offset are committed atomically. Never bypass the multi. (See ecto-expert for read-model schema design and Multi composition patterns.)
defmodule MyApp.Notifications.WelcomeEmailHandler do
use Commanded.Event.Handler,
application: MyApp.CommandedApp,
name: __MODULE__,
consistency: :eventual
alias MyApp.Accounts.Events.UserRegistered
alias MyApp.Notifications.EmailLog
# Idempotent: at-least-once delivery means this can fire twice for the
# same event. Check-before-write (or rely on a unique constraint) so
# the second delivery is a no-op.
def handle(%UserRegistered{user_id: id, email: email, name: name}, _meta) do
case MyApp.Repo.get_by(EmailLog, user_id: id, type: "welcome") do
nil ->
MyApp.Mailer.send_welcome(email, name)
MyApp.Repo.insert!(%EmailLog{user_id: id, type: "welcome"})
:ok
_already_sent ->
:ok
end
end
end
Rule: Event handlers must be idempotent — Commanded delivers at-least-once, and replaying handlers (e.g., during a reset) re-fires every event. Many handlers are naturally idempotent (upserts, Repo.insert(..., on_conflict: :nothing), projections via Ecto.Multi). Side-effecting handlers (email, webhook) need explicit deduplication.
# Eventual consistency (default — fastest)
:ok = MyApp.Router.dispatch(%RegisterUser{user_id: id, email: email, name: name})
# Strong consistency — block until all :strong handlers complete
:ok = MyApp.Router.dispatch(cmd, consistency: :strong)
# Strong against specific handlers only (best of both worlds)
:ok = MyApp.Router.dispatch(cmd, consistency: [MyApp.Accounts.Projections.UserProjection])
# With timeout and metadata
:ok = MyApp.Router.dispatch(cmd,
consistency: :strong,
timeout: 10_000,
metadata: %{user_id: current_user.id, request_id: req_id}
)
execute/2 or apply/2# BAD
def execute(state, %RegisterUser{email: email} = cmd) do
if MyApp.Repo.exists?(from u in User, where: u.email == ^email) do
{:error, :email_taken}
else
%UserRegistered{...}
end
end
Why it bites: Aggregates replay from events on cold load. If execute/2 (or apply/2) reads from the DB, replay gives different answers than the original dispatch did, and your aggregate state becomes non-deterministic. Worse, the read model may not yet exist during replay.
Instead: Enforce uniqueness via a separate "unique constraint" aggregate, a process manager that reserves the email before allowing registration, or accept the race and reject duplicates at the projection layer with a unique index.
# BAD
def handle(%OrderPlaced{} = ev, _meta) do
if ev.total > 1000 do
MyApp.Router.dispatch(%FlagForReview{order_id: ev.order_id})
end
end
Why it bites: Business rules should be enforceable on replay. An event handler runs once per event delivery; if you rebuild projections, this dispatch runs again (or doesn't, depending on handler reset). Logic in handlers is invisible to the aggregate's invariants.
Instead: Use a Process Manager. Its state is persisted, its dispatches are tracked, and its interested?/1 + handle/2 shape make the workflow auditable.
execute/2# BAD
def execute(state, %TransferFunds{} = cmd) do
MyApp.Router.dispatch(%DebitAccount{account_id: cmd.from, amount: cmd.amount})
MyApp.Router.dispatch(%CreditAccount{account_id: cmd.to, amount: cmd.amount})
%TransferInitiated{...}
end
Why it bites: Aggregates must be pure. Dispatching from inside execute/2 couples aggregates to each other, defeats replay purity, and creates cycles where one aggregate's command can recursively trigger itself.
Instead: Aggregate emits TransferInitiated. A Process Manager subscribes, and IT dispatches DebitAccount and CreditAccount, listening for confirmations or compensating events.
# BAD — field reuse
defmodule UserRegistered do
defstruct [:user_id, :email] # was previously :user_email
end
Why it bites: Old events in the store still have the old field name (or different semantics under the same name). Replay deserializes them and your apply/2 gets unexpected shapes. Production crashes on every cold start.
Instead: Add a new field (default nil), or define UserRegisteredV2 and write an upcaster (see references/event-versioning.md) that converts V1 events to V2 on read. Never mutate event semantics in place.
:strong consistency# BAD
:ok = MyApp.Router.dispatch(%RegisterUser{user_id: id, ...})
user = MyApp.Repo.get(User, id) # may be nil — projection hasn't caught up
Why it bites: Default consistency is :eventual. The dispatch returns when the event is persisted, but the projection that writes to the read model runs asynchronously. You'll see flaky tests and intermittent nils in production.
Instead: Use consistency: :strong (or consistency: [SpecificProjection]) when you need read-after-write semantics. Accept the latency cost. For most flows (background jobs, async UI updates), eventual is fine and faster.
@snapshot_version whenever the aggregate struct changes. Old snapshots are discarded and the aggregate replays from events.:strong consistency only blocks on handlers explicitly marked :strong — declaring consistency: :strong on the dispatch doesn't promote :eventual handlers. Both ends must agree:
# Handler side — declare strong on the use macro
use Commanded.Projections.Ecto, consistency: :strong
# Dispatch side — request strong
MyApp.Router.dispatch(cmd, consistency: :strong)
If only one side declares it, the dispatch returns before this handler completes.:stop from handle/2 to tear down. Otherwise zombie process managers accumulate forever.name: option is the subscription identifier in the event store.identify prefix: matters for stream naming — changing the prefix orphans existing streams. Pick the convention up front and keep it.@derive Jason.Encoder on every event struct — without it, events fail to serialize and dispatch crashes. Easy to forget on new events. Consider a use MyApp.Event macro that wraps it.Aggregate callbacks:
execute(state, command) → event | [events] | {:error, reason} | nil
apply(state, event) → new_state
Process Manager callbacks:
interested?(event) → {:start, id} | {:continue, id} | {:stop, id} | false
handle(state, event) → command | [commands] | {:error, reason} | []
apply(state, event) → new_state | {:stop, state}
error(error, command, context) → :skip | {:retry, ms} | {:stop, reason}
Event Handler callback:
handle(event, metadata) → :ok | {:error, reason}
Consistency modes (dispatch):
:eventual — default, async, fastest
:strong — block on ALL :strong handlers
[Handler1, Handler2] — block on these specific handlers only
references/event-versioning.mdreferences/process-managers.mdreferences/aggregate-tuning.mdreferences/middleware.mdreferences/testing.md