Skip to content

Introduction

Getting started

Install go-sequent, run the bank example against the in-memory store, and execute your first commands with SyncInTxn balance projections, idempotency and tenancy.

Getting started

This page installs the module, runs the bank example, and shows the pieces you will wire in your own service. Everything here uses the in-memory event store, so you do not need a database yet.

Install

go get github.com/go-sequent/go-sequent@latest

The module needs Go 1.24 or newer. It is released under the MIT license.

Run the bank example

The repository ships two bank samples.

Path Role
examples/bank/ Demo. In-memory SyncInTxn balances. Its integration tests always run.
examples/bankpg/ Production-shaped. Postgres event store and a SQL bank_account_balances table. Skipped without GO_SEQUENT_PG_DSN.

Run both test suites:

go test ./...
go test ./examples/bank/ ./examples/bankpg/ -count=1

go test ./... passes without any DSN; the Postgres tests skip themselves. To include them:

GO_SEQUENT_PG_DSN='postgres://user:pass@localhost:5432/go_sequent?sslmode=disable' \
  go test ./examples/bankpg/ -count=1 -run TestIT_BankPG

Your first commands

bank.NewDemoApp builds a wired application: command service, handlers, a balance projector and a notification workflow.

demo, err := bank.NewDemoApp(bank.DemoOptions{
    StoreFactory: func(reg *event.Registry) (eventstore.Store, eventstore.TxManager, eventstore.SnapshotStore, error) {
        s := memory.New(reg)
        return s, s, s, nil
    },
    Idempotency:     idempotency.NewMemory(),
    Authorizer:      bank.AllowBankCommands(),
    EnableTenancyMW: true,
})
if err != nil {
    log.Fatal(err)
}

ctx := tenancy.WithTenant(context.Background(), tenantID)
err = demo.App.Service.Execute(ctx, &bank.OpenAccount{
    AccountID: accountID,
    Owner:     "alice",
    Currency:  "CNY",
    Opening:   types.MustMoney("100.00", "CNY"),
})

bal, _ := demo.Projector.Balance(accountID)

After Execute returns, demo.Projector.Balance already reflects the new balance. The projector ran inside the same transaction as the event append. That property is called read-your-writes, and it is why ledger projections use SyncInTxn.

The StoreFactory hands the app’s event registry to the store so it can decode events on load. Pass the same registry you used for RegisterEvents.

The core loop in five steps

  1. Define events and register factories on event.Registry.
  2. Embed aggregate.Base in your aggregate and change state only inside appliers.
  3. Define commands and a command.Handler.
  4. Register projectors on SyncInTxn, so ledger balances update in the write transaction.
  5. Call CommandService.Execute(ctx, cmds...).

Core loop walks through what happens inside step 5. Modelling the domain builds steps 1 to 4 for the Account aggregate.

Money

Use types.Money. It serializes amounts as decimal strings and refuses to be built from a float.

opening := types.MustMoney("100.00", "CNY")
rounded := opening.Round(2)
_ = types.AssertNoFloatMoney(myCommand) // startup or test lint

See Money.

Tenant-scoped idempotency

idemp := idempotency.NewMemory() // or idempotency.NewPostgres(pool)
app := sequent.New(cfg, sequent.WithIdempotency(idemp, false))

cmd.Meta().TenantID = tenant
cmd.Meta().IdempotencyKey = "client-key-1"

A retry with the same (tenant_id, key) after a successful run is a no-op. See Idempotency.

AuthZ and audit metadata

app := sequent.New(cfg, sequent.WithAuthorizer(security.DenyByDefault{
    Allow: map[string]bool{"bank.Deposit": true},
}))

audit := command.AuditOf(cmd) // CommandID, TenantID, CorrelationID, Source, ...

See AuthZ.

Outbox and Postgres polling

Ledger projectors stay on SyncInTxn. For asynchronous fan-out, wrap the publisher with an outbox:

ob := outbox.NewMemory() // or outbox.NewPostgres(pool)
pub := &publish.WithOutbox{Inner: app.Publisher, Writer: ob, Topic: "events"}
app.Repo.Publisher = pub

Drain it with the CLI:

go run ./cmd/go-sequent outbox -dsn "$GO_SEQUENT_PG_DSN" -once

Leave out -once for a long-running poller. See Publisher.

Ops CLI

go run ./cmd/go-sequent migrate -dsn "$GO_SEQUENT_PG_DSN"
go run ./cmd/go-sequent replay  -dsn "$DSN" -projector balances -stage prepare -tables t1
go run ./cmd/go-sequent outbox  -dsn "$DSN" [-once]

See Migrations and CLI.

Next steps

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