ScalabilityData Architecture

Event Sourcing

Storing only the current state of an entity throws away the one thing a business often needs most - the history of how it got there. Audits become guesswork, "why did this happen?" becomes unanswerable, and rebuilding alternative projections of the data requires going back to the operational logs no one kept.

Rickvian Aldi·Software engineer·5 min read

Problem

A normal database stores the current value of every field. When a row changes, the previous value is overwritten and gone. For many applications this is fine - nobody needs to know the former shipping address for an order that was delivered a year ago. But for a growing class of domains, the history is the product: financial ledgers, medical records, inventory counts, compliance-regulated workflows, anything where an auditor may someday ask "why did this number look different on March 2nd?"

State-only storage cannot answer that question. Adding audit tables after the fact is fragile (someone always forgets to log a mutation), incomplete (side effects escape the audit trail), and schema-coupled (a log that mirrors the current schema drifts out of date with it). Every team that has tried to add "full history" to a state-only system has discovered that you cannot bolt a time machine onto a design that threw away the past.

Forces

  • History has business value. Regulators, support engineers, and fraud investigators frequently need to reconstruct past states. A system that cannot produce them at arbitrary points in time imposes manual reconstruction work forever.
  • Projections have a short half-life. Product teams repeatedly want to slice the same data in new ways - new dashboards, new reports, new search indices. If the source of truth is a log of events, new projections can be built from it. If the source of truth is the current state, only the shapes you anticipated can be derived.
  • Concurrency on state is hard. Two processes updating the same row race. Two processes appending to a log do not - append-only logs have simple concurrency semantics (optimistic concurrency by version, or partition-level ordering).
  • Storage is cheap; truth is expensive. The storage cost of keeping every event forever is trivial compared to the engineering cost of recovering lost history.

Solution

Event Sourcing makes the sequence of events the source of truth. Instead of a row saying "account 42: balance=$412.73," there is an append-only log: AccountOpened(42), Deposited(42, $500), Withdrawn(42, $87.27). The current balance is a function of folding those events in order. Nothing else needs to be persisted for the balance to be authoritative.

Writing:

async function withdraw(accountId: string, amount: number): Promise<void> {
  const events = await eventStore.readStream(accountId);
  const state = events.reduce(applyEvent, initialState);
 
  if (state.balance < amount) {
    throw new InsufficientFunds();
  }
 
  await eventStore.append(accountId, {
    type: 'Withdrawn',
    payload: { amount },
    expectedVersion: state.version, // optimistic concurrency
  });
}

The write path is: load the stream, fold it into state, validate the command against that state, and append a new event. No row is ever updated in place.

Reading:

Reads go through projections. A projection subscribes to the event log and maintains a read model shaped for a specific query. The same log can feed many projections - a balance table for the banking app, a daily-summary table for statements, a suspicious-activity index for fraud detection. If a new query shape is needed, a new projection is built and replayed from the log.

Current state is a cache of the event log. Treat it as such - disposable, rebuildable, and never the source of truth.

Snapshotting. Replaying a stream of thousands of events on every command gets slow. Periodically write a snapshot of the folded state at version N; load the snapshot plus events newer than N. Snapshots are performance optimizations, not truth - they can be deleted and rebuilt.

Event versioning. Events are immutable, but their schemas evolve. When a new field is added to Deposited, the fold function must handle both old and new shapes. Treat events as a versioned API to your future self: additive changes only, with explicit upcasters when the shape truly has to change.

Operational weight. Event sourcing changes deployment in real ways. Projection rebuilds must be routine and fast. The event store becomes the most critical piece of infrastructure in the system. Disaster recovery is different - restore the log, replay the projections, resume.

When NOT to Use

  • CRUD applications. A user profile, a settings screen, a content management system - domains where nobody will ever ask "what was this last Tuesday?" - do not benefit. The ceremony of event-sourcing pays back only when history is consumed.
  • Teams without the tooling maturity. Event sourcing requires disciplined schema evolution, snapshotting, projection rebuild automation, and operational visibility into the event store. Teams that cannot invest in this will regret the choice.
  • Reporting-only systems. If you need history for analytics but not for authoritative state, a change-data-capture stream into a warehouse is simpler and cheaper than making the operational system event-sourced.
  • Compliance domains where events cannot be retained. GDPR's right-to-erasure conflicts with an append-only, immutable log. Solutions exist (cryptographic shredding, tombstone events, per-user encryption keys), but they add real complexity. If the domain forbids retention, event sourcing is a poor fit.

CQRS is the natural pairing: in an event-sourced system, write operations append events and read operations go through projections - which is exactly CQRS. You can adopt CQRS without event sourcing, but event sourcing almost always implies CQRS. Outbox is less relevant when the event log is the store (the log itself serves the role of reliable publication), but systems that use event sourcing for a subset of aggregates and state storage for the rest still benefit from an outbox at the integration boundary.

References

  • Young, Greg. "Event Sourcing." Presentations and writings collected at eventstore.com
  • Fowler, Martin. "Event Sourcing." martinfowler.com/eaaDev/EventSourcing.html
  • Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013. Chapter 8 (Domain Events).
  • Kleppmann, Martin. Designing Data-Intensive Applications. O'Reilly, 2017. Chapter 11 (Stream Processing).

Related patterns

CQRS

A single data model forced to serve both writes and reads eventually deforms under the pressure - write-side schemas grow denormalized columns for dashboards, read-side queries are slowed by indexes that only exist for constraint enforcement, and neither job gets done well.

cqrsarchitectureread-modelsevent-drivenscalability

Transactional Outbox

Events published directly inside a database transaction can be lost if the broker is unavailable, leaving the database and downstream consumers permanently out of sync.

messagingdistributed-systemsconsistencyevent-driven

Get essays in your inbox

Practical deep-dives on software craft, career leverage, and building things that matter. No noise.