← All posts
·6 min read

CQRS and Event Sourcing: The Only Honest Way to Answer "What Did the Agent Actually Do?"

CQRSEvent SourcingAI Architecture

An auditor asks why an agent approved a refund in March. The database says the refund is approved — a single boolean, flipped in place, the same row it always was. It doesn't say who or what approved it, what the account balance looked like at the time, or whether a policy check passed before or after the approval. That information existed for a few milliseconds during the request and was overwritten the moment the next write landed on the same row. Greg Young's event sourcing pattern, usually paired with Bertrand Meyer's Command Query Responsibility Segregation, exists specifically to stop that information from being thrown away.

Two ideas that are almost always used together

Commande.g. IssueRefundRefundRequested#1RefundApproved#2RefundIssued#3LedgerUpdated#4Event log (append-only)projectProjectorfolds events→stateRead modelcurrent refund statusQuery"what's the status?"replay the whole log any time to rebuild this projection, or build a brand new oneWrites append facts to the log. Reads never touch the log — they hit a projection built from it

Event sourcing changes what you store: instead of a table of current state, you keep an append-only log of every fact that ever happened — RefundRequested, RefundApproved, RefundIssued, LedgerUpdated — each one immutable, each one timestamped, none of them ever overwritten. CQRS changes how you read that log: instead of querying the log directly, a projector folds the events into a read-optimized model built for exactly the questions you need to answer, and every query hits that projection, never the raw log. The two ideas aren't the same thing, but they're a natural pair — event sourcing gives CQRS something worth projecting, and CQRS gives event sourcing a fast read path that doesn't require replaying history on every query.

What this buys an agent system specifically

An agent's decision is exactly the kind of thing a mutated row is bad at preserving: it involved a policy check, a tool call, a piece of retrieved context, and a final action, and a stakeholder is going to ask about the reasoning behind it at some point after the fact, not during the request. With event sourcing, that reasoning doesn't have to be bolted on as a separate logging system that can drift out of sync with the actual state — the events are the state. RefundApproved carries the policy version that approved it, the account balance it checked against, and the tool call that triggered it, because that's what got appended when it happened. Replaying the log doesn't just tell you what the current status is; it tells you the entire sequence of facts that produced it, in order, without inference or reconstruction.

The replay property is the other half of the payoff. A regulator asks for a new report format eighteen months after the fact — a projection nobody built at the time. With row-mutation state, that's often impossible; the intermediate states are gone. With an event log, it's a new projector, run once against history that was never deleted, producing a read model that could have existed from day one if anyone had thought to ask for it then.

What it costs, honestly

Nothing here is free. The write path gets more code: every state change is an explicit, named event instead of an UPDATE statement, which means more upfront modeling of what "happened" actually means for your domain. Read performance depends entirely on projections existing and staying current — query a shape nobody projected yet and you're either waiting for a new projector to catch up or replaying the whole log on demand, which is slow at scale. And the log genuinely never shrinks; you need a real strategy for archiving or snapshotting old events, or storage costs become someone's quarterly problem.

The failure mode I've actually seen isn't underuse — it's over-scoping. Teams event-source everything, including internal state nobody will ever need to audit or replay, and pay the write-path complexity tax for it everywhere. The pattern earns its cost specifically where the history has value on its own — compliance, financial state, anything an agent decided that a human might reasonably ask "why" about later. A UI's hover state doesn't need this. An agent's refund decision does.

The row was never telling you the whole story

A mutated database row answers one question — what's true right now — and silently discards every other question you might have later. Most of the time that's a fine trade. The moment an AI system makes a decision that a person will need to explain, defend, or reproduce after the fact, it stops being fine, because "what's true right now" was never the question anyone was actually going to ask.