Reference
Repository and UnitOfWork
Reference for go-sequent's aggregate Repository and context-scoped UnitOfWork: Add, Load, Commit, identity map behaviour, factories, snapshot loading and goroutine rules.
Repository and UnitOfWork
Package: aggregate
The repository loads aggregates, tracks them in a UnitOfWork, and commits their uncommitted events through the EventStore and the publisher.
Construction and factories
repo := aggregate.NewRepository(store, publisher)
repo.RegisterFactory("bank.Account", func(id uuid.UUID) aggregate.Root {
return NewAccount(id)
})
A factory builds an empty aggregate of a given type. Load needs one to rehydrate, and snapshot rebuild needs it too. With the façade, app.RegisterAggregate(type, factory) does this for you.
Fields: Store, Snapshots (optional), Publisher, Factories.
UnitOfWork
A UnitOfWork lives on the context of one Execute. It is an identity map plus an ordered list of aggregates.
AddandLoadneed a UoW on the context. Without one they returnaggregate: no UnitOfWork in context.Loadreturns the instance already in the UoW if the id is known. You never get two mutable copies of one aggregate in oneExecute.Commitcollects uncommitted events from tracked aggregates in load order.defer uow.Clear()inExecuteempties it when the call ends, success or not.
Do not share a UoW, or any aggregate it holds, across goroutines. Parallelism belongs between requests, not inside one.
Add
acc := NewAccount(id)
if err := acc.OpenAccount("alice", "CNY", opening); err != nil {
return err
}
return repo.Add(ctx, acc)
Add tracks a new aggregate. Its events are stored at commit.
Load
agg, err := repo.Load(ctx, id, "bank.Account")
Load does the following:
- Return the tracked instance if there is one.
- Find the factory for the aggregate type. Error if none:
aggregate: no factory for type. - If a
SnapshotStoreis set, load the latest snapshot whose version matchesSnapshotVersion(), restore it, and set the aggregate version to the snapshot sequence. - Load the stream through
Store.LoadStream. Events at or before the snapshot sequence are skipped.ErrStreamNotFoundis returned if there is no stream and no snapshot. - Replay the remaining events with
LoadFromHistory. - Check
Invariants(). - Track the aggregate in the UoW.
LoadStream verifies the hash chain while it reads, so tampering surfaces here as ErrHashChainTamper.
Commit
Commit runs once per command, after the handler returns:
- For each tracked aggregate with uncommitted events, run
Invariants(). - Build a
StreamAppendper aggregate: id, type,PartitionKey,ExpectedVersion: agg.Version(), events,UniqueKeys. - Call
Store.Append(ctx, commandRecord, streams). It writes the command row and all streams atomically, and is where OCC and unique keys are enforced. - Call
Publisher.Publishwith all events, in load order and sequence order. With SyncInTxn, projectors and workflows run here, inside the transaction. - Mark each aggregate committed. Its version advances and its uncommitted list clears.
If a step fails, Commit returns the error. Execute rolls back the transaction. If nothing was uncommitted, Commit does nothing.
Event order is load order of aggregates first, then sequence order within each. It does not depend on interleaved Apply calls across aggregates.
Exists
ok, err := repo.Exists(ctx, id)
Reports whether a stream has any events.
Errors you will see
| Error | Source |
|---|---|
eventstore.ErrOptimisticConcurrency |
Append: stream tip differs from ExpectedVersion |
eventstore.ErrUniqueKeyViolation |
Append: a unique key is already taken |
eventstore.ErrStreamNotFound |
Load: unknown aggregate id |
eventstore.ErrHashChainTamper |
Load: stored events fail verification |
| invariant errors | Your Invariants() after Load or before append |
Related
Docs track the go-sequent library. When APIs drift, the source wins.