Back in December 2024 I asked here whether anyone would be interested in a book about Event Sourcing and CQRS. I had a six page table of contents and a lot of doubt about whether I wanted to commit the time.
Well, I committed the time. Two years of it.
And I did one thing differently from what I planned, which turned out to matter more than anything else: I built the implementation first and let the code tell me what the book should say.
Sounds like a small decision, right? It is not. It rewrote chapters.
Let me show you one.
Snapshotting looks simple until you build it
Everybody describes snapshotting the same way. Your event stream gets long, replaying it on every load gets slow, so you periodically save the aggregate’s state and on the next load you restore that and replay only what came after.
That description is correct. It is also useless if you are the one writing the code.
I found four things while implementing it that no diagram tells you. Three of them ended up in the chapter.
The trigger is boundary math, not a modulus
You want to snapshot every fifty events. So you check whether the version is a multiple of fifty, right? version % 50 == 0. Done.
No. And here is the part that should worry you: it fails silently.
One command appends three events. Your stream goes from version 49 to version 52. It steps right over 50 and never lands on it. The check never fires, the snapshot never happens, and nobody tells you anything, because a missing snapshot looks exactly like an aggregate that has not hit the threshold yet. The load still works. It is just slow. You find out six months later when somebody asks why one aggregate takes 400 ms to load.
What you want is to ask whether the append moved the stream into a new bucket:
postVersion / interval > preVersion / interval
Integer division, evaluated across the append instead of at a single point. A multi-event append that jumps a boundary captures once, at the post-append version.
Simple fix. But I did not see it until I wrote the test that appends three events at once.
Do not upcast your snapshots
Events get upcast. Event shapes change, you write an upcaster to lift the old shape to the new one, and you maintain that chain forever, because events are immutable and you cannot go back and rewrite history.
So when the snapshot shape changes, you upcast the snapshot too. Right?
Wrong, and I am glad I found this in code rather than in print. A snapshot is a cache. The aggregate can always rebuild it from events. So when the shape changes, let the old snapshot read as a miss, rebuild from history, and capture a fresh one at the current shape the next time you cross a boundary.
Upcasting snapshots buys you nothing that a discard does not already give you, and it costs you a second chain of upcasters to keep correct next to your event upcasters. Two lineages that have to agree with each other, forever, where zero would do.
Put the schema version in the WHERE clause, by the way. Do not read the row and compare in memory. A snapshot at a shape you cannot read should never cross the wire in the first place.
Guard the restore or corrupt your writes
This one bit harder.
RestoreFrom(snapshot, version) seats state onto your aggregate and sets its version. That version is the concurrency token your next append checks against.
Now let somebody call that on an aggregate that has already applied events, or is holding uncommitted work. You just overwrote the token. The next append goes out with an expected version describing a state the aggregate is not in, and a stale write lands as if it were current.
In an event-sourced system! Where the whole promise is that the log tells you the truth!
So the restore throws unless the aggregate is pristine. Loud failure right at the seam, instead of silent corruption three layers downstream that you find out about in production.
Your speedup test cannot be a stopwatch
Obvious test: load with a snapshot, load without, assert the first is faster.
I have the scar on this one. The same code passed five out of five on a 32-core machine and starved on a shared 4-core CI runner. The scheduler widened the window and the timing budget stopped meaning anything at all.
Snapshotting does not save you milliseconds. It saves you replays. So test replays. Seed a stream past two boundaries, load through the snapshotting repository over a store that records where the read started, and assert two things: the read began at the snapshot’s version, and it replayed strictly fewer events than the full stream.
Machine independent, and it measures what the pattern is actually for.
This happened across the whole book
The snapshotting chapter is not the chapter I outlined. Neither is the one on correlation tracing. Neither is the one on event versioning, and that one moved the most.
Every single time the implementation and the manuscript disagreed, the implementation won and the chapter got rewritten. Not once did I look at working code and decide my prose had been right all along.
And I think this is the thing nobody tells you about writing on architecture. Any pattern you can draw on a whiteboard has a layer underneath it where the real decisions live. Interval math. Concurrency tokens. What your test can honestly assert on a CI runner you do not control. You cannot write about that layer if you have not been down there, and readers can tell.
So here is my advice to anybody thinking about writing a technical book: build it first. All of it. Ship the code, run it, break it, and let it tell you what your chapters should say.
The manuscript is at 449 pages across 18 chapters now, with a production-grade reference implementation in .NET running on PostgreSQL, SQL Server, KurrentDB and DynamoDB behind one contract.
What would you want to see from a book like this? I am still listening.




