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
- Define events and register factories on
event.Registry. - Embed
aggregate.Basein your aggregate and change state only inside appliers. - Define commands and a
command.Handler. - Register projectors on
SyncInTxn, so ledger balances update in the write transaction. - 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
- What is go-sequent
- Modelling the domain
- Bank example
- Operations overview before you go to production
Docs track the go-sequent library. When APIs drift, the source wins.