Architecture Styles Advanced

Event Sourcing

Instead of persisting current state, store every state change as an immutable domain event. Current state is derived by replaying the event stream.

⏱ 10 min read

What it is

Event Sourcing is a persistence pattern where, instead of storing the current state of an entity (e.g., order.status = SHIPPED), every change to that entity's state is stored as an immutable domain event (e.g., OrderPlaced, PaymentConfirmed, OrderShipped). The entity's current state is derived by replaying all events from the beginning of its stream. The event store is the system of record; all other data stores (read models, caches) are derived projections. Event Sourcing was championed by Greg Young alongside CQRS and is well-suited to domains where the history of what happened is as important as the current state — financial ledgers, audit systems, collaborative editing, and supply chain tracking. The event store must guarantee append-only writes and ordered reads per stream.

Why it exists

Traditional state-based persistence discards the history of how an entity reached its current state. In an accounting system, knowing that an account has a balance of $500 is less useful than knowing the sequence of credits and debits that produced that balance. Event Sourcing preserves this full audit trail naturally, without separate audit tables. It also enables temporal queries ("what was the state of this order at 3pm on Tuesday?") by replaying events up to a point in time. Critically, new read models can be built at any time by replaying the full event history — you can ask questions today about data that was collected years ago, as long as the events were captured. This is a powerful capability for analytics, compliance, and product evolution.

When to use

  • Audit and compliance requirements mandate a complete, tamper-proof history of all state changes.
  • The domain models events as first-class concepts (accounting ledgers, order fulfillment, user action tracking).
  • Temporal queries are needed — "show me the state of this entity at any point in time."
  • New read models may be needed in the future based on events already collected.
  • Used in combination with CQRS where events naturally drive read-side projection updates.

When not to use

  • Simple CRUD with no audit requirements — the event store overhead is unjustified when current state is all you need.
  • High-frequency entity updates (millions of events per aggregate) — replay becomes expensive without aggressive snapshotting.
  • Teams without event sourcing experience — the operational and conceptual complexity is high, and mistakes are hard to reverse in an append-only store.
  • GDPR/right-to-erasure requirements conflict with immutable event stores unless event encryption and key deletion strategies are carefully designed.

Typical architecture

Aggregates are loaded by replaying their event stream. Commands produce new events appended to the stream. Projectors consume the event stream to update read models.

  Command
     │
┌────▼─────────────┐
│  Aggregate        │
│  Load: replay     │◀─── Event Store (stream per aggregate)
│  events → state  │      [OrderPlaced v1]
│  Apply command   │      [PaymentConfirmed v2]
│  Emit new event  │      [OrderShipped v3]
└────┬─────────────┘           │
     │ Append new event        │ Subscribe
     └─────────────────────────┘
                                │
                    ┌───────────▼──────────┐
                    │  Projectors           │
                    │  (build read models)  │
                    └───────────┬──────────┘
                                │
              ┌─────────────────┼──────────────┐
              │                 │              │
       ┌──────▼───┐   ┌─────────▼──┐  ┌───────▼──┐
       │ Orders   │   │ Analytics  │  │ Search   │
       │ View DB  │   │ DB         │  │ Index    │
       └──────────┘   └────────────┘  └──────────┘

Pros and cons

Pros

  • Complete audit trail with zero extra code — the event store is the audit log.
  • Temporal queries — reconstruct any entity's state at any point in time.
  • New read models can be retroactively built from historical event streams.
  • Debugging production issues by replaying events in a test environment.
  • Natural fit with domain events and DDD aggregate design.

Cons

  • High operational complexity: event store management, projection rebuilds, snapshotting strategy.
  • Event schema evolution is hard — events are immutable and persist forever, requiring upcasters to transform old event versions.
  • GDPR compliance is challenging — deleting personal data from an immutable event store requires encryption with key deletion.
  • Replay performance degrades for long-lived aggregates without periodic snapshots.
  • Steep learning curve — most developers lack event sourcing experience.

Implementation notes

Use a dedicated event store (EventStoreDB, Axon Server) or build on top of an append-only Kafka topic or PostgreSQL table with a stream-per-aggregate partition key. Implement snapshots for aggregates that accumulate hundreds of events — periodically save current state alongside a pointer to the last event, and start replay from the snapshot. Design event schemas carefully and version them from day one. Use upcasters to transparently transform old event versions to the current schema when replaying. For GDPR compliance, encrypt personal data fields within events using a per-user encryption key; deleting the key renders the data unreadable without deleting the event. Always include aggregate version (sequence number) in the event to enable optimistic concurrency — reject commands that conflict with an already-appended event at the same version.

Common failure modes

  • No snapshotting strategy: An order aggregate with 10,000 events takes 2 seconds to load — snapshotting should have been designed from the start.
  • Breaking event schema changes: Renaming a field in an event breaks all projectors and replayers reading old events — upcasters were never implemented.
  • Storing commands as events: Events should represent things that happened ("OrderShipped"), not commands ("ShipOrder") — conflating the two breaks the temporal semantics.
  • Over-fine event granularity: An event for every field change on every form submission creates huge streams that are meaningless from a domain perspective.
  • Missing idempotent projectors: Projectors that don't handle duplicate event delivery create inconsistent read models when events are redelivered.

Decision checklist

  • Are events in the past tense, representing domain facts ("OrderPlaced"), not commands or current state?
  • Is a snapshotting strategy defined for aggregates that will accumulate many events?
  • Are event schemas versioned and upcasters implemented for schema evolution?
  • Is there a GDPR compliance strategy (encrypted PII with key deletion) if personal data is in events?
  • Are all projectors idempotent to handle duplicate event delivery?
  • Is there a mechanism to rebuild projections from scratch (full replay)?

Example use cases

  • Banking ledger: Account balances are computed by replaying all debit and credit events — the event log is the regulatory record of truth.
  • GitHub commit history: A repository's state at any point in time is the accumulated application of all commits since the beginning — a form of event sourcing.
  • Collaborative document editor (Google Docs): Every keystroke is an operation event; the current document state is the result of replaying all operations, enabling conflict resolution and operational transformation.
  • CQRS — The canonical partner pattern: events are the write model; projections are the CQRS read models.
  • Event-Driven Architecture — Event Sourcing events can be published to an EDA broker for downstream consumers.

Further reading