Skip to content

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

  1. Copy the commands into a queue.
  2. Create the AfterCommit hook bag on the context.
  3. Start the span and timer when Obs is set.
  4. Tx.WithinTx begins the transaction.
  5. Create a UnitOfWork and attach it to the context.
  6. While the queue is not empty, take the next command and run the steps below.
  7. When the queue is empty, the transaction commits.
  8. Record telemetry.
  9. 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:

  1. tenancy.Middleware: copies the tenant from the context into Meta.TenantID.
  2. 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
}

Docs track the go-sequent library. When APIs drift, the source wins.