Why architecture matters here

The architecture matters because some domains are fundamentally about history, not just current state, and modeling them with in-place updates loses information you cannot recover. Financial ledgers, inventory movements, order lifecycles, medical records, and compliance-heavy workflows all need to answer 'how did we get here' — and once you have overwritten the old value, the answer is unrecoverable. Event sourcing makes the audit trail intrinsic rather than bolted on: because every change is an immutable event, you can always reconstruct exactly what happened, when, and in what order, without a separate audit table that can drift out of sync with reality. The log is not a copy of the truth; it is the truth.

The second forcing function is temporal flexibility. Because current state is derived by replaying events, you can derive different state by replaying them differently. You can reconstruct an entity as it was at any past moment by replaying only the events up to that time — invaluable for debugging ('what did this account look like when the charge failed?') and for point-in-time reporting. You can build a brand-new read model months after the fact by replaying the entire history through new projection logic, materializing a view that was never anticipated when the events were first written. In-place state cannot do this: it only ever holds now, and now is all you can query.

The third reason is that event sourcing pairs naturally with CQRS (Command Query Responsibility Segregation) and decouples writes from reads in a way monolithic state cannot. The write side validates commands and appends events; the read side consumes those events to build denormalized projections optimized for querying. This lets you keep multiple read models, each shaped for a different query pattern, all derived from the same event stream and each rebuildable independently. But this power comes with real cost: the system is eventually consistent (a projection lags the log), replay can be slow without snapshots, and events written today must still be interpretable by code written years from now. Event sourcing is a deliberate trade of simplicity for history, flexibility, and auditability — chosen where those properties justify the complexity, not by default.

There is a scoping discipline that separates event sourcing that succeeds from event sourcing that becomes a swamp: apply it to the few aggregates whose history genuinely matters, not to the entire system. A ledger, an order, a shopping cart benefit enormously; a user-preferences blob or a reference lookup table does not, and forcing them into events adds ceremony with no payoff. Treating event sourcing as a targeted pattern for history-critical domains rather than a house-wide architecture is the judgment call that keeps the approach an asset instead of a liability.

A related architectural insight is that event sourcing changes what a 'bug fix' means for data. In a system that stores current state in place, fixing a data error means running a migration that overwrites the wrong value — and the fact that it was ever wrong, and how, is lost. In an event-sourced system you never rewrite history; instead you append a compensating event that corrects the record going forward, and the original mistaken event stays in the log alongside the correction, fully auditable. This is not merely philosophical tidiness: in regulated domains it is a hard requirement that the record of what the system believed, and when, be immutable. It also means projection bugs are recoverable in a way in-place state bugs are not — if a read model computed a wrong balance because of a flawed fold, you fix the projection code and replay the untouched event log to rebuild a correct view, with no risk that the fix corrupts the source of truth, because the source of truth is the events and the events were never wrong. The log's immutability turns categories of data incidents from irreversible into merely inconvenient.

Advertisement

The architecture: every piece explained

Top row: the write path. A command expresses intent — ShipOrder, Deposit — and is a request that may be rejected. The aggregate is the consistency boundary that handles the command: it first rehydrates its current state from its event stream, validates the command against that state and its business rules, and if valid emits one or more events describing what happened. The event store is the append-only log, organized as a stream per aggregate instance; appending an event is the only write operation, and events are immutable once written. The event bus publishes newly appended events to subscribers — projections, other services, integrations — so the rest of the system learns of the change.

Middle row: deriving state and views. Rehydrate is how an aggregate gets its current state: replay its events in order, folding each into a running state, so the aggregate is always reconstructed from the log rather than read from a state table. Snapshots keep this bounded — periodically the aggregate's derived state is saved so rehydration starts from the latest snapshot and replays only the events since, instead of the entire history. Projections consume the event stream to build read models: denormalized, query-optimized views (a customer's order list, an account balance table) that the read side serves. Each projection is a fold over events into a shape convenient for one class of query, and can be rebuilt from scratch by replaying the log.

Bottom rows: correctness and longevity. Optimistic concurrency protects concurrent appends: a command reads the aggregate at version N and appends expecting the stream to still be at N; if another writer already appended, the expected-version check fails and the command retries against fresh state, so two concurrent commands never silently clobber each other. Schema evolution handles the fact that events are immutable and eternal: as the model changes, events are versioned and old events are transformed on read by upcasters into the current shape, so decade-old events remain readable. The ops strip names the signals that matter: append latency, projection lag, replay time, and storage growth.

Event sourcing — store the sequence of changes, derive state by replaythe append-only event log is the source of truth; current state is a fold over its eventsCommandintent to changeAggregatevalidate + emit eventsEvent storeappend-only log per streamEvent buspublish to subscribersRehydratereplay events -> stateSnapshotsperiodic state checkpointProjectionsbuild read models (CQRS)Read modelsqueryable viewsOptimistic concurrencyexpected version on appendSchema evolutionversioned events + upcastersOps — append latency + projection lag + replay time + storage growthrehydrateemitpublishnotifysnapshotprojectserveoperateoperate
Event sourcing: commands are validated by an aggregate that emits events appended to a per-stream event store and published on a bus; state is rehydrated by replaying events (accelerated by snapshots), projections build queryable read models, and optimistic concurrency plus versioned events guard correctness and evolution.
Advertisement

End-to-end flow

Trace a deposit through an event-sourced account. A Deposit(amount=100) command arrives for account 42. The command handler loads aggregate 42: it fetches the latest snapshot (state as of version 300) and replays the handful of events since, rehydrating the account to its current balance and version 305. It validates the command — the account is open, the amount is positive — and, satisfied, emits a Deposited(amount=100) event. It appends that event to stream 42 with an expected-version of 305. The event store checks: is stream 42 still at version 305? Yes, so the append succeeds, the event becomes version 306, and it is immutable from now on.

The append triggers publication. The new Deposited event flows onto the event bus, and a balance projection consumes it: it reads the account's current balance in its read model and adds 100, writing the updated balance row. A separate statement projection appends a line to account 42's transaction history view. Neither projection is the source of truth — both are derived — so if either is ever wrong or a new one is needed, it can be dropped and rebuilt by replaying stream 42 from the beginning. A user querying their balance reads the projection's row, fast and denormalized, never touching the event log directly.

Now concurrency. Two deposits for account 42 arrive at nearly the same instant. Both handlers rehydrate to version 306. Both validate and try to append expecting version 306. The first append wins and moves the stream to 307. The second append's expected-version check fails — the stream is no longer at 306 — so the store rejects it. The second handler catches the conflict, re-rehydrates to the now-current version 307 (which includes the first deposit), re-validates its command against fresh state, and appends again as version 308. No deposit is lost and none is applied twice; optimistic concurrency turned a potential lost update into a clean retry.

Finally, replay in anger. Months later, product wants a new view: a monthly-average-balance report that was never built. There is no migration to write and no historical data to backfill from a warehouse — the events already hold everything. A new projection is deployed and pointed at the start of the log; it replays every event for every account through its new logic, materializing the report as if it had always existed. Separately, an auditor asks what account 42 looked like on a specific past date; the system replays stream 42's events only up to that date's cutoff and reconstructs the exact historical state. Neither answer was anticipated when the events were written, and both are available precisely because the log kept the whole history instead of only the latest value.