effect-cqrs
Event sourcing and CQRS for Effect v4. Commands decide on events, events are the only source of truth, and read models are projections you can throw away and rebuild.
pnpm add effect-cqrs effectnpm install effect-cqrs effectyarn add effect-cqrs effectbun add effect-cqrs effectA trial port of moleculer-cqrs to Effect v4, so the API may change.
- #1AccountOpened{ owner: "Alice" }
- #2MoneyDeposited{ amount: 250 }+250
- #3MoneyWithdrawn{ amount: 40 }−40
- #4TransferSent{ to: "bob", amount: 60 }−60
- #5MoneyDeposited{ amount: 120 }+120
- #6TransferRefunded{ amount: 60, reason: "AccountNotOpen" }+60
{ status: "open", owner: "Alice", balance: 330, version: 6 }How a command flows
Command
A typed call, or JSON from a transport through
Commands guidedispatch. The payload is checked against its schema first.Aggregate
The service rebuilds the aggregate's state from its events, and the handler decides what happened, or fails with a typed error.
Aggregates guideEvent store
The events are appended, but only if nobody wrote to that aggregate since its state was loaded.
Storage guideView models
Each one reads the new events from its own checkpoint.
View models guidecaughtUpwaits until your own write is there.
Beyond one command
The path above is the core. These modules cover what a system needs around it, and each is opt-in.
- Saga
Sagas across aggregates
A process that touches several aggregates, like a transfer between two accounts, runs as a durable workflow that an event starts. The engine keeps the outcome of every step, so a restart skips the commands it already ran, and compensations undo the steps that succeeded.
- PersonalData
GDPR, by crypto-shredding
Forget a person without rewriting the log. Personal fields are encrypted with a key that belongs to their subject, and
forgetdestroys it. The events stay, with their amounts and dates, and streams still replay. - CommandApi
An HTTP API from an aggregate
A
POSTendpoint per command, with history and state, as anHttpApiGroupwith OpenAPI.Idempotency-KeyandIf-Matchreach the command, and failures map to status codes. - Outbox · EventCatalog
Events for other systems
An outbox publishes events to Kafka, NATS, SQS and the like, at least once and in order. The catalog describes them as JSON Schema and AsyncAPI, and its guard fails when a change can no longer decode what is stored.
- EventVersions
Events that change shape
The log is never rewritten. Declare the older shapes of an event and how each becomes the next, and old events are upcast as they are read. Handlers and projections only see the current one.
- ViewModel · Leases
Made to keep running
Command ids make a retried command run once. View models retry a failing handler, park an event that never passes, and run on one replica at a time. Every event records its cause, and the services report metrics.
A bank, running in your browser
Accounts are aggregates, three view models project them, and a saga moves money between them. The customer app on the left only shows read models, so you can watch them trail the store. The devtools on the right slow every stage down, pause and step commands, crash a projection, and replay the log.
Tests read like moleculer-cqrs
Fixture runs commands through the real service on an in-memory store, so validation and encoding are tested too. The chain is an Effect, which it.effect runs.
it.effect("deletes a todo", () => Fixture.make(Todo) .givenEvents([created]) .when("deleteTodo", {}) .expectEvent("TodoDeleted") .inspectState((state) => assert.isTrue(state.deleted)))Keep the log where it fits
Every store passes the same tests. The numbers come from one laptop, so read them as orders of magnitude.
- In memoryFor tests, examples and the browser.60,000 commands/s
- SQLiteOne process, one file, no server.0.1 ms a command
- PostgreSQLSeveral processes writing, positions in commit order.1 ms a command