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: OrderCreated → ItemAdded → PaymentAuthorized → OrderShipped. 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_v2as 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 🚀
- For event contracts and schema evolution: see the event design guide.
- For schema registry as a practical tool for versioning and compatibility: see the Schema Registry guide.
- For governance and cataloging: see the governance guide.
- For operations, retries, and DLQ: see the operational guide.