Guides
Bank example
A tour of go-sequent's two bank samples: the in-memory demo in examples/bank and the Postgres-backed examples/bankpg with SQL balances, tenancy, idempotency and AuthZ.
Bank example
The repository has two bank samples with the same domain. Read the first to learn the shapes. Read the second to see what a Postgres-backed service looks like.
| Path | Role |
|---|---|
examples/bank/ |
Demo. In-memory SyncInTxn balances. Integration tests always run. |
examples/bankpg/ |
Production-shaped. Postgres event store and SQL balances through projector.Persistor. |
What the demo covers
examples/bank implements four commands: open account, deposit, withdraw and transfer. It shows:
- Decimal money through
types.Money. - A tenant-scoped unique key: one account per owner per tenant.
- AuthZ with a deny-by-default allow-list.
- Tenant-scoped idempotency.
- A
SyncInTxnbalance projector. - A workflow that schedules an AfterCommit hook.
- JSON snapshots on the
Accountaggregate.
Files
| File | Contents |
|---|---|
events.go |
Five event types and RegisterEvents |
account.go |
The Account aggregate, appliers, command methods, UniqueKeys, snapshot methods |
commands.go |
OpenAccount, Deposit, Withdraw, Transfer with Validate |
handler.go |
Handler that routes commands to aggregate methods |
projector.go |
BalanceProjector (in-memory) |
workflow.go |
NotifyWorkflow using RegisterAfterCommit |
app.go |
NewDemoApp, DemoOptions, AllowBankCommands |
Modelling the domain builds these files step by step.
Wiring with NewDemoApp
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,
})
ctx := tenancy.WithTenant(ctx, tenantID)
_ = demo.App.Service.Execute(ctx, &bank.OpenAccount{ /* ... */ })
bal, _ := demo.Projector.Balance(accountID)
Inside NewDemoApp:
sequent.NewwithSyncInTxnand the authorizer.RegisterEvents(app.Events).- The store comes from
StoreFactory, which receives the app’s event registry. - Tenancy middleware is added first when
EnableTenancyMWis set. Idempotency middleware is added after it, soMeta.TenantIDis populated by then. Repo.Store,Repo.Publisher,Service.TxandService.Repoare set.- The aggregate factory, projector, workflow and handler are registered.
AllowBankCommands() returns security.DenyByDefault with the four bank command types allowed. Any other command type is denied.
The production-shaped sample
examples/bankpg keeps the same domain and swaps the infrastructure:
- Postgres event store opened with
Openand prepared withstore.Migrate. - A table
bank_account_balances, owned by the example, not by the core schema. SQLBalanceProjector, which writes throughprojector.Persistor. The persistor joins the write transaction, so balances still update inside the transaction.- Tenancy middleware, Postgres idempotency, and the AuthZ allow-list.
- Optional
obs.InstrumentsthroughOptions.Instruments.
Apply the core schema and then the example’s projection DDL:
store.Migrate(ctx)
bankpg.MigrateProjection(ctx, store.Pool)
Known gaps
The sample is honest about what it leaves out:
- No row-level security policies.
- No HTTP or gRPC. go-sequent is a pure library.
- Projection rebuild in the sample is a manual truncate plus stream replay helper used by tests. It is not the full
projector/replaypipeline with golive. See Projection rebuild. - The outbox is not wired by default. Ledgers stay on
SyncInTxn. Wrappublish.WithOutboxyourself if you need asynchronous fan-out. - The OpenTelemetry
RecordEventscounter is not called automatically on append.
Running the tests
# Always runs. Postgres tests skip without a DSN.
go test ./examples/bank/ ./examples/bankpg/ -count=1
# With Postgres
GO_SEQUENT_PG_DSN='postgres://user:pass@localhost:5432/go_sequent?sslmode=disable' \
go test ./examples/bankpg/ -count=1 -run TestIT_BankPG
Covered scenarios: command to events to SyncInTxn projection, idempotent retry, AuthZ denial, tenant-scoped unique owner, and OCC. The memory demo always runs these. The SQL balance version runs when a DSN is set.
Use it as a template
When you start a new service, copy examples/bankpg and change the domain. Keep the wiring order: tenancy middleware first, then idempotency, AuthZ as a filter, SyncInTxn for anything that feeds a balance. Move non-ledger side effects to a Workflow AfterCommit hook or the outbox.
Docs track the go-sequent library. When APIs drift, the source wins.