Skip to main content

CQRS

VEF includes a lightweight CQRS bus with typed handlers and behavior middleware.

Public API

The public CQRS package mainly exposes:

  • cqrs.BaseCommand
  • cqrs.BaseQuery
  • cqrs.Register(...)
  • cqrs.Send(...)
  • cqrs.Behavior
  • cqrs.BehaviorFunc
  • vef.ProvideCQRSBehavior(...)

Supporting public APIs:

APIPurpose
cqrs.NewBus(behaviors)create a standalone bus, mostly useful in tests or custom wiring
cqrs.Bushandler registry and dispatcher abstraction
cqrs.Action / ActionKindaction contract; Command and Query are the exported kind constants
cqrs.Handler[TAction, TResult] / HandlerFunc[...]typed handler contracts
cqrs.Behavior / BehaviorFunccommand/query execution pipeline
cqrs.Orderedoptional behavior ordering hook
cqrs.Unitempty result type for commands
ErrHandlerNotFoundno handler registered for the action type
ErrResultTypeMismatchhandler 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 bandUse
0..99transactional / contextual setup that must wrap everything
100..199audit / collector lifecycle
200..299event 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.