Skip to content

Reference

Command

Reference for go-sequent's Command interface and Meta: command types, aggregate ids, tenant and idempotency fields, audit snapshots with AuditOf, and Validate hooks.

Command

Package: command

A command is an intent to change the system. It has a type name, an optional primary aggregate id, and metadata. Commands enter through CommandService.Execute, where middleware, AuthZ and optional validation run before a handler sees them.

The interface

type Command interface {
    CommandType() string
    AggregateID() (uuid.UUID, bool)
    Meta() *Meta
}
  • CommandType() returns a stable name such as bank.Deposit. The router and AuthZ allow-lists use it.
  • AggregateID() returns the primary aggregate and whether it is set. It is stored with the command record.
  • Meta() returns the command’s metadata.

Embed command.Base to get Meta():

type Deposit struct {
    command.Base
    AccountID uuid.UUID   `json:"account_id"`
    Amount    types.Money `json:"amount"`
}

func (c *Deposit) CommandType() string { return "bank.Deposit" }
func (c *Deposit) AggregateID() (uuid.UUID, bool) {
    return c.AccountID, c.AccountID != uuid.Nil
}

Base holds a Meta value in field M. Base.Meta() calls EnsureMeta, which fills defaults on first use.

Meta

Field Role
CommandID Unique id of this command. Used for audit and as the foreign key target for idempotency rows.
IdempotencyKey Optional. Per-tenant idempotency key. Requires TenantID.
TenantID Tenant isolation unit. Usually copied from the context by tenancy middleware.
UserID Actor issuing the command.
CorrelationID Groups a business conversation.
CausationID Links to the command or event that caused this one.
CausedByAggregate, CausedBySequence Set when a workflow triggers the command.
IssuedAt Time the command was issued.
Source Audit label such as api, workflow, admin.
AuthContext Opaque claims. Not necessarily persisted.

EnsureMeta(m) fills CommandID with a new UUID when empty, IssuedAt with the current UTC time, and CorrelationID with CommandID.

Do not trust a client-supplied UserID on its own. Authentication belongs to your host application. Put verified claims in AuthContext and check them in an Authorizer.

Validation

Implement Validate() error to check input shape before the handler runs:

func (c *Deposit) Validate() error {
    if c.AccountID == uuid.Nil {
        return fmt.Errorf("account_id required")
    }
    if c.Amount.IsZero() || c.Amount.IsNegative() {
        return fmt.Errorf("amount must be positive")
    }
    return nil
}

CommandService detects the command.Validator interface and calls it after filters and before dispatch. Rules that need aggregate state belong in aggregate methods, not here.

Audit snapshot

command.AuditOf(cmd) returns an AuditSnapshot suitable for persistence:

audit := command.AuditOf(cmd)
// audit.CommandID, CommandType, TenantID, UserID,
// IdempotencyKey, CorrelationID, CausationID, Source, IssuedAt

On a successful command the store also writes a command record (commands table) through command.RecordOf. It carries the id, type, aggregate id, tenant, user, idempotency key, issue time, and the command payload as JSON.

Money in commands

Use types.Money for any amount field. testkit.BanFloat64Money(cmd) and types.AssertNoFloatMoney(cmd) fail when a command or event has a float64 money field. Run one in a test or at startup. See Money.

Multiple commands in one call

err := svc.Execute(ctx, cmd1, cmd2) // one transaction, one UnitOfWork

Commands run in order. Workflows may enqueue more commands with command.Enqueue; they run in the same transaction after the ones already queued. See Workflow.

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