CQRS + Event Sourcing: A Practical Guide to Scalable Data Architectures

CQRS + Event Sourcing: A Practical Guide to Scalable Data Architectures
Table of Contents
- Why CRUD Fails at Scale
- CQRS: The Core Concept
- Command Side: The Write Model
- Event Sourcing: Storing Events Not State
- Query Side: Building Read Models with Projectors
- The Outbox Pattern: Atomic Commands + Events
- Eventual Consistency: Handling the Delay
- Event Versioning and Upcasting
- Tooling: EventStoreDB, Axon Framework, Marten
- When to Apply CQRS + Event Sourcing
- Frequently Asked Questions
- Key Takeaway
Why CRUD Fails at Scale
Standard CRUD (Create, Read, Update, Delete) uses a single model for all data operations:
Problems at scale:
- Lock contention: Write locks on
usersblock reads during updates - Index bloat: Adding indexes to speed reads slows writes
- Schema coupling: The table must serve all query shapes (dashboard, API, reports)
- Audit limitations: You can see current state, but not history of changes
CQRS: The Core Concept
The fundamental insight: commands and queries have completely different access patterns and optimization requirements. Stop fighting over the same data model.
Command Side: The Write Model
The command side validates business rules and records what happened as an immutable event:
Event Sourcing: Storing Events Not State
In a traditional system, you store current state: orders.status = 'SHIPPED'.
In Event Sourcing, you store the history of facts:
To get current state: Replay all events:
The superpowers this gives you:
| Feature | CRUD | Event Sourcing |
|---|---|---|
| Audit log | Add extra audit table (often forgotten) | Built-in — every change is an event |
| Time travel | Cannot see past states | Replay up to any point in time |
| Debug production | "Why does this record look like this?" | Read the event history — the entire story |
| Business analytics | Aggregate from current snapshots | Query event stream directly |
| Undo | Complex compensating logic | Apply reverse event |
Query Side: Building Read Models with Projectors
A Projector (also called Event Handler or Read Model Builder) listens to events and builds optimized views for reads:
Multiple read models from the same events:
OrderDashboardProjector→ MongoDB for order management UIOrderAnalyticsProjector→ Elasticsearch for search and filteringOrderRevenueProjector→ Separate PostgreSQL table for finance reporting
The Outbox Pattern: Atomic Commands + Events
A critical challenge: how do you ensure the event is published to a message bus only if the command succeeded?
Problem: Two separate operations = potential inconsistency:
Outbox Pattern Solution: Write the event to an outbox table in the same database transaction, then relay it asynchronously:
Eventual Consistency: Handling the Delay
Between a command being accepted and the read model being updated, there's a short delay. REST APIs need to handle this gracefully:
When to Apply CQRS + Event Sourcing
Apply when you have:
- Complex domain with many business rules that change state
- Strict audit requirements (finance, healthcare, legal)
- Read and write loads differ by 10:1 or more
- Multiple downstream consumers need notifications of state changes
- "What was the state at time T?" is a real business requirement
Do NOT apply for:
- Simple CRUD apps (todo lists, basic CMS, admin dashboards)
- Small teams without distributed systems experience
- Greenfield projects where the domain is not yet understood
- Systems without audit or compliance requirements
Frequently Asked Questions
Does CQRS always require Event Sourcing? No — they are complementary but independent patterns. You can use CQRS with a traditional relational database on the write side (just update the table and also update a separate read model). Event Sourcing provides the best write-side storage for CQRS, but it's not required. Many teams apply CQRS first, then introduce event sourcing as the auditing/replay requirements emerge.
What happens if a Projector crashes mid-update? Projectors should be idempotent — processing the same event twice produces the same result. Event stores assign each event a sequence number. Projectors track the last processed sequence number in their own store. On restart, they resume from where they stopped. This guarantees at-least-once processing, and idempotency ensures at-most-once effect.
Key Takeaway
CQRS + Event Sourcing is the architectural pattern for systems where data history, audit, and scale matter. The event log becomes the single source of truth — current state is just a materialized view. The complexity cost is real: you need to handle eventual consistency, design projectors, manage event schemas, and deal with the Outbox pattern. But for banking, healthcare, trading, and logistics systems — where the history of why data is in a state is as important as the state itself — there is no alternative that scales as elegantly.
Read next: Sharding Patterns: How to Scale Your Database to Terabytes →
Part of the Software Architecture Hub — comprehensive guides from architectural foundations to advanced distributed systems patterns.
