Skip to content

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

KeywordCategoryMeaningHTTP Semantics
alreadyIdempotentThe outcome has already been achieved200 or 409
forbiddenBusiness rulePolicy prohibits this action403
impossibleStructuralThe precondition does not exist400

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 patternReplacement
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"

Architecture decisions