Back to blog
Ai News

Event Sourcing Cqrs

5 min read

Event Sourcing and CQRS get mentioned together so often that it's easy to assume they're one pattern. They're not — they solve different problems, they can each be adopted independently, and understanding that separation is the key to deciding whether either (or both) is worth the real complexity they add to a system.

What each pattern actually does

Event Sourcing stores every change to an entity's state as an immutable event, rather than storing just the current state. Instead of a users table row that gets overwritten every time a user updates their profile, you store a sequence of events — UserCreated, EmailChanged, AddressUpdated — and the current state is derived by replaying those events in order. This gives you a complete audit trail for free, and the ability to reconstruct what the state looked like at any point in the past (temporal queries).

CQRS (Command Query Responsibility Segregation) separates the models used for writing data (commands) from the models used for reading data (queries). Instead of one unified model serving both, writes go through a command model optimized for handling business logic and validation, and reads are served from a separate, often denormalized, model optimized purely for query performance.

When each one is worth it on its own

Event Sourcing makes sense when:

  • You have a genuine audit requirement — financial transactions, healthcare records, anything where "what changed and when, provably" is a business or legal requirement, not just a nice-to-have.
  • You need temporal queries — showing what an entity's state looked like at a specific point in time, not just its current state.
  • Your domain has complex logic where the sequence of events matters more than the final state — e.g., an order that was modified three times tells a different story than an order that arrived in its final form directly.
  • You're building an event-driven architecture already and want historical replay capability as a natural extension.

CQRS makes sense when:

  • Your read and write workloads have very different performance or scaling characteristics — e.g., writes are relatively rare and complex, but reads are extremely frequent and need to be fast, so a single shared model is a compromise for both sides.
  • Your business rules and query needs change frequently, and you want to evolve read and write models independently without one change rippling into the other.
  • You need to support multiple, very different read representations of the same underlying data (a dashboard view, a detailed audit view, a search index) without contorting one model to serve all of them.

Why they're often combined — and the real cost of doing so

Event sourcing naturally pairs with CQRS because once you're storing state as a stream of events, you often want to materialize that stream into one or more read-optimized "projection" views — which is exactly what CQRS's separate query model is for. Combined, this lets you independently scale writes (append-only event stream) and reads (however many materialized views you need), and it gives you both the audit trail from event sourcing and the query performance from CQRS.

The real cost is complexity, and it's not small. You now have to handle eventual consistency between the write side and any read projections — a read model rebuilt from an event stream isn't necessarily up to date the instant an event is written, and your application has to handle that gap correctly. You also add operational surface area: event schema versioning (what happens when UserCreated needs a new field years into production), replay logic, and projection rebuild processes that don't exist in a simpler CRUD system.

Production patterns worth knowing before you build this

If you do decide event sourcing and CQRS are justified, two implementation details separate a system that works reliably from one that generates subtle data-consistency bugs. The first is avoiding dual writes: a common and dangerous anti-pattern is writing an event to your database and separately publishing it to a message broker like Kafka as two independent operations, because a failure between those two writes leaves your event store and your event stream out of sync with no clean way to detect it. The safer pattern is the transactional outbox — write the event and an "outbox" row to your primary database in a single transaction, then use a poller or a change-data-capture tool like Debezium to reliably push that outbox row to Kafka afterward, so the event's existence in your system of record and its eventual delivery to consumers are never split across two separate, independently-failable writes.

The second is snapshotting. Without it, reconstructing an entity's current state means replaying its entire event history from the beginning every time you need to read it — fine for an entity with a dozen events, genuinely slow for one with tens of thousands accumulated over years. The standard fix is periodic snapshotting: storing a materialized "state as of event N" checkpoint alongside the stream, so reconstruction only needs to replay events since the last snapshot rather than the full history. Log compaction — retaining only the most recent snapshot per entity and discarding older ones — keeps this from becoming its own unbounded storage problem over time.

A related warning from current practitioner guidance is worth repeating explicitly: the most common mistake isn't choosing the wrong tooling, it's applying event sourcing and CQRS globally across an entire system rather than selectively to the specific bounded contexts that actually need the audit trail or the read/write scaling split. Most of a typical application genuinely does not need this pattern, even in a system where one particular domain (payments, for instance) clearly does.

The honest recommendation

For most CRUD-shaped applications — a typical SaaS product, an internal admin tool, most e-commerce backends — a straightforward relational model with good indexing solves the actual problem without the operational overhead of event sourcing and CQRS. These patterns earn their complexity specifically when you have a real audit/compliance requirement, a genuine temporal-query need, or a read/write scaling mismatch severe enough that a shared model is actively causing pain. Adopting them because they're architecturally interesting, without one of those concrete drivers, is a common and expensive mistake — the complexity shows up immediately, and the benefit only shows up if you actually hit the problem they're designed to solve.

Sources: Microsoft Azure Architecture Center — Event Sourcing Pattern, Calmops — Event Sourcing vs CQRS, Conduktor — CQRS and Event Sourcing with Kafka, OneUptime — How to Implement Event Sourcing with Kafka

Keep reading

Get new posts as they publish

No spam — just the next post, straight to your inbox.

Discussion