Skip to content

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 effect

A trial port of moleculer-cqrs to Effect v4, so the API may change.

account/aliceappend only
  1. #1AccountOpened{ owner: "Alice" }
  2. #2MoneyDeposited{ amount: 250 }+250
  3. #3MoneyWithdrawn{ amount: 40 }−40
  4. #4TransferSent{ to: "bob", amount: 60 }−60
  5. #5MoneyDeposited{ amount: 120 }+120
  6. #6TransferRefunded{ amount: 60, reason: "AccountNotOpen" }+60
state = events.reduce(projection, initial){ status: "open", owner: "Alice", balance: 330, version: 6 }

How a command flows

  1. Command

    A typed call, or JSON from a transport through dispatch. The payload is checked against its schema first.

    Commands guide
  2. Aggregate

    The service rebuilds the aggregate's state from its events, and the handler decides what happened, or fails with a typed error.

    Aggregates guide
  3. Event store

    The events are appended, but only if nobody wrote to that aggregate since its state was loaded.

    Storage guide
  4. View models

    Each one reads the new events from its own checkpoint. caughtUp waits until your own write is there.

    View models guide

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 forget destroys it. The events stay, with their amounts and dates, and streams still replay.

  • CommandApi

    An HTTP API from an aggregate

    A POST endpoint per command, with history and state, as an HttpApiGroup with OpenAPI. Idempotency-Key and If-Match reach 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)))
Testing guide

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
Storage guidePerformance