Rejection Clauses
When a decide clause covers a (Command, State) pair that should never produce events, a rejection clause expresses why using one of three keywords: already, forbidden, or impossible.
Syntax
decide(<Command>, <State>) -> already "<message>"decide(<Command>, <State>) -> forbidden "<message>"decide(<Command>, <State>) -> impossible "<message>"The three categories
| Keyword | Category | Meaning | HTTP Semantics |
|---|---|---|---|
already | Idempotent | The outcome has already been achieved | 200 or 409 |
forbidden | Business rule | Policy prohibits this action | 403 |
impossible | Structural | The precondition does not exist | 400 |
impossible
The command references something that does not exist in this state. The transition is structurally incoherent. The command has no precondition to act upon.
Use impossible when the state says “that thing does not exist here.” Example: depositing into an account that has not been opened.
forbidden
The command is coherent — the precondition exists — but business policy prohibits the action. The state is present; the action is disallowed.
Use forbidden when the state says “that exists, but we do not allow this here.” Example: withdrawing from an overdrawn account.
already
The command’s intended outcome has already been achieved. The domain is already in the target state. Retrying the command would produce no new information.
Use already when the state says “that is already done.” Example: registering a user who is already active.
Complete example
context Registration { command Register { userId: UserId email: Email name: DisplayName } command Deactivate { userId: UserId reason: Reason } event Registered { userId: UserId email: Email name: DisplayName } event Deactivated { userId: UserId reason: Reason }
decider User { commands: Register, Deactivate events: Registered, Deactivated state: Unregistered | Active(email: Email, name: DisplayName) | Inactive(reason: Reason) initial: Unregistered terminal: Inactive
decide(Register, Unregistered) -> [Registered { userId, email, name }] decide(Deactivate, Unregistered) -> impossible "Cannot deactivate an unregistered user" decide(Register, Active) -> already "User is already registered" decide(Deactivate, Active) -> [Deactivated { userId, reason }]
evolve(Unregistered, Registered) -> Active evolve(Unregistered, Deactivated) -> Inactive evolve(Active, Registered) -> Active evolve(Active, Deactivated) -> Inactive }}Rejection semantics
A rejection is not an event. Events represent facts that happened. A rejection means nothing happened — the event stream remains untouched and no state transition occurs.
This distinction matters for event sourcing: replaying an event stream must always produce the same state. Rejections are not part of the stream.
Exhaustiveness
Rejection clauses count as coverage for the exhaustiveness check. Every (Command, State) pair in a decider must have either a decide clause that emits events or a rejection clause. A missing pair is a compile error.
Missing decide clause for (Deactivate, Unregistered) in decider User.Add: decide(Deactivate, Unregistered) -> already|forbidden|impossible "<message>" or: decide(Deactivate, Unregistered) -> [<Event> { ... }]Migration from require false
The require false else reject "<message>" pattern is deprecated. The compiler produces a hard error when it encounters it:
require false is deprecated. Use: -> already|forbidden|impossible "message"Replace each require false block with the rejection keyword that matches the domain reason:
| Old pattern | Replacement |
|---|---|
require false else reject "already done" | -> already "already done" |
require false else reject "not allowed" | -> forbidden "not allowed" |
require false else reject "does not exist" | -> impossible "does not exist" |