Skip to content

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 SyncInTxn balance projector.
  • A workflow that schedules an AfterCommit hook.
  • JSON snapshots on the Account aggregate.

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:

  1. sequent.New with SyncInTxn and the authorizer.
  2. RegisterEvents(app.Events).
  3. The store comes from StoreFactory, which receives the app’s event registry.
  4. Tenancy middleware is added first when EnableTenancyMW is set. Idempotency middleware is added after it, so Meta.TenantID is populated by then.
  5. Repo.Store, Repo.Publisher, Service.Tx and Service.Repo are set.
  6. 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 Open and prepared with store.Migrate.
  • A table bank_account_balances, owned by the example, not by the core schema.
  • SQLBalanceProjector, which writes through projector.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.Instruments through Options.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/replay pipeline with golive. See Projection rebuild.
  • The outbox is not wired by default. Ledgers stay on SyncInTxn. Wrap publish.WithOutbox yourself if you need asynchronous fan-out.
  • The OpenTelemetry RecordEvents counter 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.