Command Handler Skill
Scope: command handlers in an event-driven architecture — handlers load an aggregate, the aggregate emits domain events ([[domain-event]]), and state + events are persisted through the transactional outbox, with idempotency mandatory ([[event-idempotency]]). If your project is simple CQRS without events or event sourcing, this is the wrong skill: use the analogous cqrs-command skill from the clean-ddd pack instead — the two architecture packs are alternatives, not layers.
When to use
Adding a new use case that mutates state, refactoring a CRUD endpoint into an intent-revealing command, or splitting a service that mixes commands and queries.
Command naming (normative)
- Imperative, intent-revealing:
PlaceOrder, CancelOrder, ApprovePayment.
- NEVER
UpdateOrder / ChangeOrder — CRUD verbs hide intent and produce meaningless events downstream.
- One command = one intent. Two intents → two commands.
- A command is a request — it can be rejected. Its successful outcome is a fact in past tense:
PlaceOrder → OrderPlaced.
Handler responsibilities (the process)
- Dedup at entry: check the command ID (or rely on aggregate version) — see [[event-idempotency]].
- Validate preconditions: structure was checked at the transport boundary; here check business preconditions only.
- Load the aggregate from the repository (by ID, at a known version).
- Invoke one aggregate method — on success it records events (e.g.
OrderPlaced); on rule violation it returns a domain error.
- Persist atomically: aggregate state + drained events into the outbox in one transaction (the handler is the transaction boundary).
- Return acknowledgement — success/failure and the aggregate ID. NOT data; reads belong to [[event-projection]] queries.
What a command handler does NOT do
- Does NOT query read models or projections (stale by design — never a basis for writes).
- Does NOT return domain entities or display DTOs.
- Does NOT publish events directly to the broker — only the outbox relay publishes ([[domain-event]]).
- Does NOT call other use cases or services in sequence — multi-step flows are a [[saga-orchestrator]].
- Does NOT span multiple aggregates in one transaction — the consistency boundary is a single aggregate; cross-aggregate consistency is eventual, via events.
Idempotency (mandatory)
Commands arrive more than once: client retries, broker redelivery, saga re-dispatch. Pick at least one strategy:
- Command ID dedup: deterministic command ID, unique-constraint insert at handler entry; conflict → return previous result.
- Aggregate version (optimistic concurrency): command targets
expected_version; stale command → rejected, no duplicate effect.
- Natural invariants: the aggregate itself refuses a repeat (
Place() on an already-placed Order is a no-op or domain error).
Details and a decision table in [[event-idempotency]].
Failure handling (normative)
- Business rule violation → domain error, do NOT retry: retrying won't make an empty order valid.
- Infrastructure failure (DB down, lock timeout) → safe to retry; idempotency makes the retry harmless.
- Concurrency conflict (version mismatch) → reload and re-decide, or surface to the caller; never blind-merge.
Anti-patterns to avoid
- Query logic inside a command handler (mixing the read path back in).
- Handler chaining — a handler dispatching the "next" command; that's hidden choreography, promote it to a [[saga-orchestrator]].
- Long-running operations (external HTTP calls, waits) inside the handler transaction — offload to a saga step.
- Multi-aggregate transactions "just this once".
- Dual write: publishing to the broker inside the handler instead of via outbox.
- Returning read data from a command "to save a round-trip".
Verification
Going deeper
- reference.md — idempotency strategy decision table, optimistic concurrency mechanics, retry/backoff semantics, direct dispatch vs command bus, testing with testcontainers.
- examples.md —
PlaceOrder handler with dedup, aggregate invocation, and single-transaction outbox write, in Go and TypeScript.
1---2name: command-handler3description: Command Handler Skill4---5# Command Handler Skill67> **Scope:** command handlers in an **event-driven** architecture — handlers load an aggregate, the aggregate emits domain events ([[domain-event]]), and state + events are persisted through the transactional outbox, with idempotency mandatory ([[event-idempotency]]). If your project is simple CQRS without events or event sourcing, this is the wrong skill: use the analogous `cqrs-command` skill from the clean-ddd pack instead — the two architecture packs are alternatives, not layers.89## When to use1011Adding a new use case that mutates state, refactoring a CRUD endpoint into an intent-revealing command, or splitting a service that mixes commands and queries.1213## Command naming (normative)1415- **Imperative, intent-revealing:** `PlaceOrder`, `CancelOrder`, `ApprovePayment`.16- NEVER `UpdateOrder` / `ChangeOrder` — CRUD verbs hide intent and produce meaningless events downstream.17- One command = one intent. Two intents → two commands.18- A command is a **request** — it can be rejected. Its successful outcome is a fact in past tense: `PlaceOrder` → `OrderPlaced`.1920## Handler responsibilities (the process)21221. **Dedup at entry**: check the command ID (or rely on aggregate version) — see [[event-idempotency]].232. **Validate preconditions**: structure was checked at the transport boundary; here check business preconditions only.243. **Load the aggregate** from the repository (by ID, at a known version).254. **Invoke one aggregate method** — on success it records events (e.g. `OrderPlaced`); on rule violation it returns a domain error.265. **Persist atomically**: aggregate state + drained events into the outbox **in one transaction** (the handler is the transaction boundary).276. **Return acknowledgement** — success/failure and the aggregate ID. NOT data; reads belong to [[event-projection]] queries.2829## What a command handler does NOT do3031- Does NOT query read models or projections (stale by design — never a basis for writes).32- Does NOT return domain entities or display DTOs.33- Does NOT publish events directly to the broker — only the outbox relay publishes ([[domain-event]]).34- Does NOT call other use cases or services in sequence — multi-step flows are a [[saga-orchestrator]].35- Does NOT span multiple aggregates in one transaction — the consistency boundary is a single aggregate; cross-aggregate consistency is eventual, via events.3637## Idempotency (mandatory)3839Commands arrive more than once: client retries, broker redelivery, saga re-dispatch. Pick at least one strategy:4041- **Command ID dedup**: deterministic command ID, unique-constraint insert at handler entry; conflict → return previous result.42- **Aggregate version (optimistic concurrency)**: command targets `expected_version`; stale command → rejected, no duplicate effect.43- **Natural invariants**: the aggregate itself refuses a repeat (`Place()` on an already-placed Order is a no-op or domain error).4445Details and a decision table in [[event-idempotency]].4647## Failure handling (normative)4849- **Business rule violation** → domain error, do NOT retry: retrying won't make an empty order valid.50- **Infrastructure failure** (DB down, lock timeout) → safe to retry; idempotency makes the retry harmless.51- **Concurrency conflict** (version mismatch) → reload and re-decide, or surface to the caller; never blind-merge.5253## Anti-patterns to avoid5455- Query logic inside a command handler (mixing the read path back in).56- Handler chaining — a handler dispatching the "next" command; that's hidden choreography, promote it to a [[saga-orchestrator]].57- Long-running operations (external HTTP calls, waits) inside the handler transaction — offload to a saga step.58- Multi-aggregate transactions "just this once".59- Dual write: publishing to the broker inside the handler instead of via outbox.60- Returning read data from a command "to save a round-trip".6162## Verification6364- [ ] Command name is imperative and intent-revealing; resulting event is past tense.65- [ ] Handler is thin: business rules live in the aggregate, not the handler.66- [ ] State + events committed in one transaction through the outbox.67- [ ] Replaying the same command produces no duplicate effects (test it).68- [ ] Domain errors are not retried; infra errors are retryable.6970## Going deeper7172- **[reference.md](reference.md)** — idempotency strategy decision table, optimistic concurrency mechanics, retry/backoff semantics, direct dispatch vs command bus, testing with testcontainers.73- **[examples.md](examples.md)** — `PlaceOrder` handler with dedup, aggregate invocation, and single-transaction outbox write, in Go and TypeScript.