Blog
CQRS with effect-cqrs
Section titled “CQRS with effect-cqrs”A series in five parts, November 2026. We build a small lending library, where members
borrow books and bring them back, from the first event to a saga, tests and a database.
Every snippet comes from
examples/blog,
whose tests run it.
1. Why CQRS and event sourcingWhat goes missing when you store only the current state, event sourcing by hand in forty lines, and what CQRS adds on top.
2. Your first aggregateA book as an aggregate: events as schemas, commands that decide, typed errors, and a store that handles concurrent writers.
3. Read models you can throw awayView models that follow the log, catch up after a restart, read your own writes, and rebuild from scratch.
4. SagasBorrowing a book touches the member and the book, so a saga coordinates them as a durable workflow.
5. Tests, schema changes and productionGiven, when, then and random commands, an event that changes shape, SQLite and PostgreSQL, and forgetting a member.
When things go wrong
Section titled “When things go wrong”Bugs and incidents every team meets, on the same library: what they do to a database that keeps only the current state, and what the log changes. Each post stands alone, and its example runs in a test of its own.
Fix the code, not the dataA loans table with a bug that loses loans, fixed in a database, where the lost rows stay lost, and in a read model, where a replay brings them back.
The wrong eventA bug in a command stores wrong due dates for a week. Why not to edit the log, hide it in an upcaster or patch the read models, and how a correction event fixes it.
The double writeStore the loan and tell the broker, in either order, and see what each breaks on a bad day. With event sourcing the log is already the outbox.
I clicked twiceA payment request times out and is sent again. Why disabling the button and a unique column fall short, and how a command id makes a retry return the first answer.
The form that overwritesTwo librarians edit the same record and the second save undoes the first. Commands that say what changed avoid most collisions, and an expected version catches the rest.
The message that blocks the queueOne event a handler can't read stops everything behind it. Stop and wait, or park it and carry on: when each is right, and how a parked event comes back after the fix.
What it looked like last TuesdayA member was refused a book that is on the shelf today. The log shows the book as it was at that moment, and every event the request caused, across aggregates.
The bug that won't reproduceA member can't borrow anything and every test passes. Copy her stream from production into a fixture, and the bug report becomes a failing test.
Two pods, two emailsScaled to two replicas, every member gets every email twice. Background work that reads the log must run in one place, and move when that place goes away.
Design choices
Section titled “Design choices”Decisions that shape an event-sourced system before anything breaks: where a process lives, how to read, what to keep, and how to get there from the tables you already have.
The saga you skippedBorrowing a book without a saga, in the two ways that look right, what breaks when a write fails between them, and what the saga does instead.
Three ways to readA lent book read from its aggregate, from a read model as it is, and from a read model that waits for the command, in one process and over HTTP.
Forget me, from the backups tooA member asks to be erased, and her name is still in every backup, ready to come back with the next restore. Crypto-shredding erases her from the backups you already have.
From a table to a logEvery project that adopts event sourcing already has data in tables. Import each row as the first event of its stream, invent no history, and switch writes over one aggregate at a time.
Change the rules, keep the historyThe loan period changes and every open loan's due date moves with it. Store the decisions rules made, run rules only when deciding, and never in a projection.