The Manuscript Is Done. The Code Is Public.

In August I wrote that you should build the thing first and let the code tell you what the chapters say. That post was a bet made in the middle of the work. This one is the report.

The manuscript is finished: 452 pages, 18 chapters, 72 diagrams, 79 code listings. Not a draft I am still circling. Finished.

More useful to you: the reference implementation it was built on is now public. It lives at github.com/ThomasJaeger/event-sourcing-cqrs, MIT licensed, .NET 10 and C# 14, tagged v1.0.0, which is the release the book describes.

What is actually in the repository

It models an order-management domain across five bounded contexts, event-sourced end to end. Five aggregates rebuilt from events. Two process managers that are themselves event-sourced on their own streams, with compensation branches. Eight projections maintaining read models over a mix of relational tables and JSONB. Four hosts: a Blazor Server UI, a JSON API, a workers host carrying projections and the outbox, and an admin console. Role-based authorization and tenant isolation run through all of them.

Fifty-three architecture decision records carry the reasoning. If you only read one part of the repository, read those. They are where the arguments are, including the ones I lost.

It is licensed so you can lift pieces of it into commercial work without asking me.

The part that cost the most

Four event stores sit behind one interface as first-class peers: PostgreSQL hand-rolled over Npgsql, SQL Server hand-rolled over Microsoft.Data.SqlClient, KurrentDB over gRPC, and DynamoDB with conditional writes. All four pass the same contract suite. Switching between them is a configuration change.

That constraint was the most expensive decision in the project and the one I would make again. The moment a second store has to satisfy the same contract, every convenience that leaked out of the first one becomes visible. Optimistic concurrency stops being a paragraph and becomes four different mechanisms that have to produce the same guarantee. You cannot hand-wave a contract test suite.

It also answers the question I get most often, which is some version of “do I have to adopt a new database for this?” No. The domain model does not have to change when that answer changes.

Where the book stands

It is in submission. No publisher is named here, there is no publication date, and I am not going to invent one. When there is a contract to announce, I will announce it.

I am saying that plainly because the alternative is the thing I find tiresome in this corner of the industry: announcements engineered to imply more than they say. The manuscript being finished is a fact. The code being public is a fact. A publication date is not yet a fact.

What you can do with this today

Clone it. Run it. The README is honest about what docker compose up does and does not start, which is the kind of detail that usually costs a reader an afternoon.

Then tell me where it is wrong. Not where it is unfamiliar, where it is wrong. Open an issue. The book is finished, but the implementation continues on main past the tag, and a reference implementation that nobody argues with is not much of a reference.

One thing it will not do is convince you to event-source your whole system. A good chunk of the book is about where these patterns do not belong, and the honest answer for a lot of software is a boring table you update in place. Chapters that only sell the pattern are how teams end up with an event store full of StatusUpdated.


I run Legacy to Modern LLC, where I modernize MS-DOS, Visual Basic, Delphi and .NET applications. If your .NET application is on a current runtime but was designed as CRUD over tables, that is a specific problem with a specific path out.

Write the code first. Let it tell you what the book says.

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.

Proposed book about Event Sourcing & CQRS

For years, I wish I had a source that I can direct people to learn more about Event Sourcing & CQRS. Greg Young never created a book, sadly. The difficulty in Event Sourcing & CQRS is not so much the technical side but, as usual, the people you need to convince and work with. Typically, Event Sourcing allows for capabilities in your software that are extremely hard or almost impossible to do with old style software development. It is fairly easy to convince executives, product, sales, etc. that these new capabilities and insights for your customers can easily beat competition. So, why is Event Sourcing not used in like 85% of all software projects? I believe that there are different reasons but at the core, where can people go to? The Web? The Web shows all sorts of different views and many of them show inexperience and brushing Event Sourcing off as “too difficult” or “increased complexity”. WTF? Software by it’s definition is hard to do properly if you stay aligned with the business and the vision your organization has. I argue that Event Sourcing makes this actually easier, not harder!!! The CRUD virus shows its ugly head again!

So, after years of not having a great source for people to direct to, I’ve been seriously thinking of writing a book about Event Sourcing and CQRS. I have not pulled the trigger because I know this a huge time commitment. However, I did create a six page table of contents and surprised myself of how much information can and should be shared on this topic. This book would be very comprehensive and covers Event Sourcing from every angle including the business and customers side and not just the technical side.

My question would be, are you interested in this type of book?

Update on my book

I have increased my efforts to complete my draft for my book “The Abundance of Object Persistence“. The increased efforts are mostly through additional time provided by my current employer RDA Corporation. I will continue small blog posts around the object persistence topic based on my book. I will post these entries at the RDA Architecture Evangelist Team Blog which I am part of as well.

If you have ideas or comments, please provide them. I welcome different point of views as this is usually a hot topic in the IT community.