entropik.
§ 02.02 · architecture · events · pillars

Event Sourcing

I tried CRUD first. Of course I did — it's what you reach for. This is what taught me why it doesn't work for AI-first platforms, and what I ended up using instead.

Starting where everyone starts

My first two AI platforms were CRUD underneath. I wasn't being careless; CRUD is what you reach for. The domain has entities, the entities have state, state changes over time, you model it as tables and you write endpoints that update rows. This is the shape of most software I've ever written and most of the time it's correct.

It stopped being correct the moment the product was really an AI loop wearing a domain hat. I didn't see this immediately. The symptom, for a while, was that the platform just felt shallow — users used it, it worked, but it never got smarter. We'd ship a prompt tweak, things would improve a bit, we'd ship another, the gains would compound less and less. Looking back, it's obvious what was happening: the signal I needed to learn from had been erased on every write.

What CRUD erases

When a user modifies an AI proposal, the interesting thing is the delta — what the AI suggested, what the human changed it to, the gap between them. That delta is the training signal. It's also the audit trail. It's also the observation that would let me answer "why did we do it this way?" six months later when I'm debugging a strange outcome.

CRUD erases the delta the moment the write lands. The row goes from A to B; A is gone. The proposal that was there before the user edited it is gone. The reasoning that led to the proposal is gone. All that remains is the end state — which, for a traditional transactional system, is exactly what you want. For an AI-first system, it's the one thing you can't afford to throw away.

Worse, in a CRUD world, new features are new mutable tables, new endpoints, new migration ceremony. The data model ossifies around the original product shape and fights you every time the product needs to learn something new. I had a three-day outage once from a "small" schema migration that cascaded across four services. Event streams would have made that a projection change, which is minutes, not days. I didn't know that at the time.

What I use now

Every domain interaction is an immutable event appended to a single stream. A question is asked. A skill executes. A user gives feedback. A path is chosen. Each of those writes a row that is never updated, never deleted, never re-interpreted after the fact. The stream is the ground truth.

State, when the product needs it, is derived from the stream as a projection — a pure function that replays events into a view. A user profile, a dashboard, a feature flag configuration: all projections. They can be rebuilt at any point by replaying history, which means a bug in a projection is a bug in code, not a corruption of data.

I'll admit this sounds more expensive than CRUD, and the first time I did it I was braced for a worse developer experience. It turned out to be the opposite at product speed, because new features no longer required schema churn. You add a new event type (if the interaction really is new) or a new projection (if you're asking an old stream a new question). The event schema becomes a small, disciplined, long-lived artefact, while the product surface above it can evolve freely.

What the discipline actually buys

Three things, all of them structural rather than incidental.

A complete training signal by construction. Every AI proposal and every human decision is on the stream with its full context. There is no separate analytics pipeline, no telemetry to bolt on, no sheepish "we forgot to log that" moment six months later. I had that moment more than once before I committed to event sourcing, and it's a particular kind of awful — the thing you need to learn from happened, and you threw it away because the write path didn't know you needed it.

An immutable audit trail. Regulators and lawyers need to know what the system did and why. Event sourcing answers that question for free, because the system has never done anything other than append to the stream. Mutation isn't a sometimes-thing you carefully avoid — it's structurally impossible.

Learning over time. Projections can be re-derived as the product's understanding of its own users improves. A pattern that only becomes visible after six months of interactions can still be surfaced, because the raw events are still there, intact. I've had this save a feature launch more than once. I've never had it hurt.

The discipline, honestly

The cost is a discipline I still find myself negotiating with: never UPDATE, never DELETE except for a right-to-be-forgotten purge. Compensating events correct prior state; history is never rewritten. I enforce this at the database level now, because I've learned that if the discipline isn't a property of the system, it's a property of whichever engineer is writing the next query at the end of a long day — and on any long enough timeline, that engineer is me, and I will cut the corner.

The platforms I built before I understood this have CRUD tables I wouldn't put there today. On Clairo, specifically, there are operational state tables that should be event-sourced and aren't; I wrote them before I'd had the realisation properly. Those are design debts I haven't yet repaid, and I'm trying not to add any more.

// continue the thought

Want to think through how this lands in your project? Tell kr8 what you’re working with.

0 / 4000 chars
kr8 · next

// Keep reading the playbook?

TOPOLOGY