Skip to content
Event Sourcing and CQRS

Event Sourcing and CQRS: when history matters 🏛️

Event Sourcing and CQRS are among the most cited—and most misunderstood—patterns in the EDA ecosystem. They usually appear together as if they were a single package. They are not: they work well together, but they have different motivations and costs.

Event Sourcing: the log is the source of truth 📜

    flowchart LR
  CMD[Command] --> AGG[Aggregate]
  AGG -->|emits| E1[Event A v1]
  AGG -->|emits| E2[Event B v1]
  E1 --> STORE[(Event Store)]
  E2 --> STORE
  STORE --> PROJ["Projection<br/>(current state)"]
  STORE --> AUDIT[Audit log]
  

In a traditional system, you store the current state of an entity. With Event Sourcing, you store the sequence of events that produced that state. The current state is a projection of history.

Example: OrderCreatedItemAddedPaymentAuthorizedOrderShipped. The order state is reconstructed by applying the events in sequence.

Concrete advantages ✅

  • Native audit trail: the history is the data itself; no extra logging tables are required.
  • Temporal queries: you can reconstruct state at any point in time.
  • Replay and re-projection: if business logic changes, you can rebuild the read views from the original data.
  • Debugging: you know exactly what happened, in what order, and why—without inferring from a snapshot.

Real costs (the ones you do not see in slides) 💸

  • Event store: you need storage optimized for append (Kafka, EventStoreDB, or Postgres with a dedicated append-only table).
  • Snapshotting: without snapshots, reconstructing the state of an entity with 10,000 events is slow.
  • Schema evolution: events are immutable, but your domain is not. Versioning and transformations are mandatory, not optional.
  • Learning curve: the team must change the way it thinks. Not everyone finds it natural to reason in events instead of CRUD on tables.

Snapshotting: do not start from zero every time 📸

When the history of an aggregate becomes long, reapplying every event on every access becomes expensive. The solution is a snapshot: a picture of the aggregate state saved at regular intervals. On the next read, you start from the latest snapshot and apply only the following events.

    flowchart LR
  SNAP["Snapshot<br/>(state at T=1000)"] --> E1001[Event 1001]
  E1001 --> E1002[Event 1002]
  E1002 --> STATO[Current state]
  

Practical strategy: create snapshots every N events or after a certain time interval. The snapshot does not replace the original events—it is a read optimization, not a deletion of history.

CQRS: when reads and writes need different models ⚖️

    flowchart TD
  C[Command<br/>(write side)] --> AGG[Aggregate / Write Model]
  AGG --> ES[Event Store]
  ES --> PROJ1[Projection A<br/>read model 1]
  ES --> PROJ2[Projection B<br/>read model 2]
  Q1[Query 1] --> PROJ1
  Q2[Query 2] --> PROJ2
  

CQRS (Command Query Responsibility Segregation) separates the write model (commands → events) from the read model (projections optimized for queries).

  • The write model is normalized, consistent, and oriented toward business invariants.
  • The read models are denormalized and optimized for specific query patterns.

Why not use a single model? 🧠

In simple systems, a single model is fine. CQRS makes sense when:

  • read queries require structures radically different from writes;
  • read traffic is much higher than write traffic and you want to scale them separately;
  • you need multiple views of the same data (dashboards, search, notifications) with different requirements.

Eventual consistency in projections 🕐

Projections are updated asynchronously after the events are published. There is always a lag between writing and the updated read state.

Practical consequences:

  • the interface must communicate eventual consistency properly (for example, “operation in progress”);
  • plan reprocessing of projections with guaranteed idempotency;
  • choose explicitly where to calculate denormalizations (in the event producer or in the consumer/projection).

Event Sourcing + CQRS: combo or alternatives? 🤔

They are often seen together, but they are independent:

Event Sourcing Without Event Sourcing
With CQRS Classic combo: events as write side, projections as read model Possible: write to an RDBMS, separate read model
Without CQRS Rare but possible: ES for audit/replay, state read from the event store Traditional system

The ES + CQRS combo works very well when:

  • you need audit/replay (the ES motivation);
  • you have read patterns that differ from event structure (CQRS motivation);
  • the domain is genuinely event-centric and not masked CRUD.

Schema evolution on immutable events 🧬

In Event Sourcing, events are immutable: you cannot modify an event that has already been written. This makes schema evolution more delicate than in a CRUD system.

Practical strategies:

  • Upcasting: when reading an old event, transform it into the new format before processing it. Implemented in the read layer, not in storage.
  • Explicit versioning: OrderCreated_v1, OrderCreated_v2 as distinct types, with separate handlers for each version.
  • Weak schema: use a flexible format such as JSON and write the read layer defensively against optional fields.

There is no free solution: choose based on the expected frequency of domain changes and the number of consumers reading the event store.

When not to use ES/CQRS 🛑

These patterns are not suitable for every system. Avoid them if:

  • the domain is essentially CRUD without requirements for audit or complex business logic;
  • the team is not ready for the operational complexity (event store, snapshots, projection reprocessing);
  • you do not need temporal queries, rebuild-from-scratch, or regulatory audit;
  • the system does not reach a scale where separating read and write is worth the cost of ownership.

Before adopting ES + CQRS, ask yourself: “Would I need to reconstruct state from zero in the future?” If the honest answer is no, a Kafka event log plus a good read model is probably all you need.

Checklist before adopting ES/CQRS ✅

  • Do you have real requirements for audit, replay, or temporal queries?
  • Have you chosen the event store (Kafka, EventStoreDB, Postgres append-only)?
  • Do you have a snapshotting strategy for long-lived aggregates?
  • Has the team understood the paradigm shift from classic CRUD?
  • Have you planned schema evolution and upcasting for future events?
  • Have you defined projection reprocessing with verified idempotency?
  • Have you measured (or estimated) projection lag and communicated expectations to product and UX?

Next steps 🚀

Last updated on