CQRS
VEF includes a lightweight CQRS bus with typed handlers and behavior middleware.
Public API
The public CQRS package mainly exposes:
cqrs.BaseCommandcqrs.BaseQuerycqrs.Register(...)cqrs.Send(...)cqrs.Behaviorcqrs.BehaviorFuncvef.ProvideCQRSBehavior(...)
Supporting public APIs:
| API | Purpose |
|---|---|
cqrs.NewBus(behaviors) | create a standalone bus, mostly useful in tests or custom wiring |
cqrs.Bus | handler registry and dispatcher abstraction |
cqrs.Action / ActionKind | action contract; Command and Query are the exported kind constants |
cqrs.Handler[TAction, TResult] / HandlerFunc[...] | typed handler contracts |
cqrs.Behavior / BehaviorFunc | command/query execution pipeline |
cqrs.Ordered | optional behavior ordering hook |
cqrs.Unit | empty result type for commands |
ErrHandlerNotFound | no handler registered for the action type |
ErrResultTypeMismatch | handler result could not be converted to the requested result type |
Action Kind Contract
Action.Kind() returns the action discriminator. BaseCommand.Kind() returns
Command (0), and BaseQuery.Kind() returns Query (1).
Defining actions
Commands embed cqrs.BaseCommand; queries embed cqrs.BaseQuery:
type CreateUser struct {
cqrs.BaseCommand
Name string
}
type GetUser struct {
cqrs.BaseQuery
ID string
}
Registering handlers
Handlers are registered by action type, and sends are type-safe:
package useractions
import (
"context"
"github.com/coldsmirk/vef-framework-go/cqrs"
)
type CreateUser struct {
cqrs.BaseCommand
Name string
}
type CreateUserHandler struct{}
func (CreateUserHandler) Handle(ctx context.Context, cmd CreateUser) (cqrs.Unit, error) {
return cqrs.Unit{}, nil
}
func RegisterHandlers(bus cqrs.Bus) {
cqrs.Register(bus, CreateUserHandler{})
}
func Run(ctx context.Context, bus cqrs.Bus) error {
_, err := cqrs.Send[CreateUser, cqrs.Unit](ctx, bus, CreateUser{Name: "alice"})
return err
}
Register uses the concrete TAction type as the registry key and panics if
another handler is already registered for the same action type. Send dispatches
by that same action type. If no handler exists, it returns an error matching
ErrHandlerNotFound and a zero-value result. Handler errors are propagated
unchanged.
HandlerFunc is a function adapter for Handler; its Handle method simply
calls the wrapped function.
Behavior pipeline
The CQRS bus supports middleware-like behaviors around command/query execution.
Use vef.ProvideCQRSBehavior(...) to register them into the runtime:
vef.ProvideCQRSBehavior(NewLoggingBehavior)
This is the right place for:
- logging
- tracing
- metrics
- cross-cutting validation
- transaction wrapping
Minimal behavior example:
func NewLoggingBehavior() cqrs.Behavior {
return cqrs.BehaviorFunc(func(ctx context.Context, action cqrs.Action, next func(context.Context) (any, error)) (any, error) {
return next(ctx)
})
}
BehaviorFunc is a function adapter for Behavior; its Handle method simply
calls the wrapped function. A behavior receives the original action and can
short-circuit without calling next. If it short-circuits with nil, Send
returns the zero value for TResult. If it short-circuits with a non-nil value
whose concrete type is not TResult, Send returns an error matching
ErrResultTypeMismatch instead of panicking.
Behaviors are sorted once when the bus is built. Ordered.Order() controls
wrapping order: lower values wrap outside higher values. Behaviors that do not
implement Ordered default to order 0; equal orders preserve the input order
for a standalone NewBus, but FX value-group ordering is not stable. The
framework reserves these conventional bands:
| Order band | Use |
|---|---|
0..99 | transactional / contextual setup that must wrap everything |
100..199 | audit / collector lifecycle |
200..299 | event publish / outbox side effects |
1000+ | custom host behaviors |
When to use it
CQRS is optional. It is most useful when you want:
- explicit command/query boundaries
- type-safe dispatch
- a pipeline around application actions outside the HTTP resource layer
What it doesn't do
The CQRS bus does not scan for handlers automatically. You still need to register each handler into the shared bus explicitly in your module wiring.
Next step
Read Transactions if your behaviors need to wrap command execution in database boundaries.