Reference
CommandService
Reference for go-sequent's command.Service: Execute semantics, middleware order, filters, validation, the command queue, AfterCommit hooks, and how Obs attaches.
CommandService
Package: command
command.Service is the single entry point for writes. One call to Execute is one database transaction and one UnitOfWork.
Shape
type Service struct {
Middleware []Middleware
Filters []Filter
Router *Router
Repo Committer // normally *aggregate.Repository
Tx eventstore.TxManager
Obs *obs.Instruments // optional
}
err := svc.Execute(ctx, cmd1, cmd2) // one txn, one UoW
Router and Tx are required. Execute returns an error if either is nil. With no commands it returns nil immediately.
You normally get a configured service from sequent.New. See Wiring.
Execute flow
- Copy the commands into a queue.
- Create the AfterCommit hook bag on the context.
- Start the span and timer when
Obsis set. Tx.WithinTxbegins the transaction.- Create a UnitOfWork and attach it to the context.
- While the queue is not empty, take the next command and run the steps below.
- When the queue is empty, the transaction commits.
- Record telemetry.
- If there was no error, run the AfterCommit hooks.
For each command:
EnsureMeta
→ Middleware chain (outer-first)
→ Filters (AuthZ)
→ Validate (if the command implements it)
→ Router.Dispatch → Handler
→ Repo.Commit
Any error aborts the whole call. The transaction rolls back, nothing is stored, and AfterCommit hooks do not run.
Middleware
type Middleware func(ctx context.Context, cmd Command, next HandlerFunc) error
Chain runs middleware outer-first: index 0 runs first. Typical order:
tenancy.Middleware: copies the tenant from the context intoMeta.TenantID.- Idempotency middleware: checks
(TenantID, IdempotencyKey).
Tenancy goes before idempotency because the idempotency key space depends on the tenant. The bank demo does exactly this. sequent.WithIdempotency appends to Service.Middleware, so prepend tenancy yourself:
app.Service.Middleware = append(
[]command.Middleware{tenancy.Middleware},
app.Service.Middleware...,
)
A middleware can short-circuit by returning without calling next. The idempotency middleware does this for a replayed key.
Filters
type Filter interface {
Filter(ctx context.Context, cmd Command) error
}
Filters run inside the middleware chain, before validation. sequent.WithAuthorizer(auth) installs security.Filter{Auth: auth}. A denial returns security.ErrDenied. See AuthZ.
Validate and dispatch
If the command implements Validator, Validate() runs. Router.Dispatch then looks up the handler by CommandType(). No handler is an error.
Commit
After dispatch, Repo.Commit(ctx, RecordOf(cmd)) appends events and publishes them. If the error is ErrOptimisticConcurrency and Obs is set, the service calls RecordConflict first. See Repository.
The command queue
A workflow can enqueue another command during the transaction:
command.Enqueue(ctx, followUp)
Enqueued commands run after the commands already in the queue, in the same transaction and UnitOfWork. See Workflow.
AfterCommit hooks
Hooks registered with workflow.RegisterAfterCommit(ctx, fn) run only after the transaction commits. A hook error is returned from Execute, but the committed events stay. Use hooks for work that must not roll back the ledger.
Batching
svc.Execute(ctx, cmd1, cmd2, cmd3)
All commands share one transaction. The OpenTelemetry label for a batch is the first command type plus +batch. If any command fails, none are stored.
Observability
Obs is a field, not middleware. Set it with sequent.WithObs(inst) or assign svc.Obs = inst. See OpenTelemetry.
Handling errors
err := svc.Execute(ctx, cmd)
switch {
case errors.Is(err, eventstore.ErrOptimisticConcurrency):
// retry with backoff; Load again
case errors.Is(err, eventstore.ErrUniqueKeyViolation):
// business duplicate
case errors.Is(err, security.ErrDenied):
// authorization failure
case idempotency.IsReplay(err):
// Strict mode: key already processed
}
Related
Docs track the go-sequent library. When APIs drift, the source wins.