CommandHandler¶
Type¶
type CommandHandler[C Command] func(ctx context.Context, command C) (AppendResult, error)
A CommandHandler[C] is a function that handles a specific command type C. It:
- Loads the event stream for the aggregate.
- Rebuilds state via
Evolver[T]. - Applies business rules via
Decider[T, C]. - Persists the resulting events.
- Returns
AppendResultor an error.
NewCommandHandler¶
func NewCommandHandler[T any, C Command](
store EventStore,
initial InitialState[T],
evolve Evolver[T],
decide Decider[T, C],
opts ...CommandHandlerOption,
) CommandHandler[C]
Creates a CommandHandler[C] from the three functional pieces.
Parameters¶
| Parameter | Type | Description |
|---|---|---|
store |
EventStore |
Loads and saves events. |
initial |
InitialState[T] |
Factory for the zero state. Called once per command execution. |
evolve |
Evolver[T] |
Rebuilds state from a single event. Called for each event in the stream. |
decide |
Decider[T, C] |
Returns events to persist, or an error to reject the command. |
opts |
...CommandHandlerOption |
Optional configuration (see below). |
Type parameters¶
| Parameter | Constraint | Description |
|---|---|---|
T |
any |
Aggregate state type. |
C |
Command |
Command type. |
Functional types¶
InitialState[T]¶
type InitialState[T any] func() T
Returns a new instance of the initial aggregate state. Called before loading events to avoid sharing state between concurrent command executions.
var initialState = func() taskState { return taskState{} }
Evolver[T]¶
type Evolver[T any] func(currentState T, envelope *Envelope) T
A pure function that applies one event to the current state and returns the new state. Must not have side effects.
func evolve(state taskState, env *eventsourcing.Envelope) taskState {
switch env.Event.(type) {
case *events.TaskCreated:
return taskState{Exists: true}
}
return state
}
Decider[T, C]¶
type Decider[T any, C Command] func(state T, cmd C) ([]Event, error)
Enforces business rules. Returns:
- A non-empty slice of events to persist if the command is accepted.
- An empty slice (and nil error) for no-op commands (e.g. already in the desired state).
- An error to reject the command — wrapped in ErrBusinessRuleViolation.
func decide(state taskState, cmd CreateTask) ([]eventsourcing.Event, error) {
if state.Exists {
return nil, fmt.Errorf("task already exists")
}
return []eventsourcing.Event{&events.TaskCreated{...}}, nil
}
Options¶
WithStreamState¶
func WithStreamState(rev StreamState) CommandHandlerOption
Sets the concurrency check for saving events. Default: Any{}. See Stream states.
WithRetryStrategy¶
func WithRetryStrategy(strategy backoff.BackOff) CommandHandlerOption
Retries the entire load-evolve-decide-save cycle on StreamRevisionConflictError. Default: no retries.
import "github.com/cenkalti/backoff/v4"
eventsourcing.WithRetryStrategy(
backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5),
)
WithMetadataExtractor¶
func WithMetadataExtractor(fn func(ctx context.Context) map[string]any) CommandHandlerOption
Adds key-value pairs to every event envelope's Metadata. Multiple extractors are merged. See How to add metadata.
WithStreamNamer¶
func WithStreamNamer(namer StreamNamer) CommandHandlerOption
Overrides the stream name for this handler. Default: cmd.AggregateID(). See How to customise stream names.
StreamNamer¶
type StreamNamer func(ctx context.Context, cmd Command) string
var DefaultStreamNamer StreamNamer // overrideable global default
Error behaviour¶
| Situation | Error type |
|---|---|
decide returns an error |
Wrapped in *ErrBusinessRuleViolation |
| Stream revision mismatch | *StreamRevisionConflictError (retried if strategy set) |
| Store load failure | error with context |
| Store save failure | error with context |