CQRS & Event Sourcing
Architecture Diagram
When CQRS Is Worth the Complexity
Command Query Responsibility Segregation (CQRS) is not a default architecture. It is a tool for specific problems. Reach for it when:
- Read and write patterns look very different. Your writes involve complex domain operations with business rules, but reads are simple denormalized views (dashboards, reports, search results).
- Read and write loads scale independently. Maybe you have 100x more reads than writes, or your writes need strong consistency while reads can tolerate eventual consistency.
- You need multiple read representations. The same data gets queried in fundamentally different ways: by user, by time range, by aggregate, or through full-text search.
If your CRUD app reads and writes the same shaped data at similar volumes, CQRS just adds complexity for no real benefit.
Event Sourcing Fundamentals
Instead of storing the current state of an entity (balance: $500), event sourcing stores the sequence of events that produced that state:
AccountOpened { accountId: "123", owner: "Alice" }
MoneyDeposited { accountId: "123", amount: 1000 }
MoneyWithdrawn { accountId: "123", amount: 300 }
MoneyWithdrawn { accountId: "123", amount: 200 }
To get the current balance, you replay all events: 0 + 1000 - 300 - 200 = $500. What you get in return is a complete, immutable history of everything that happened.
Architecture Components
A CQRS + Event Sourcing system has these key pieces:
- Command Handler receives commands (CreateOrder, ShipOrder), validates business rules against the current aggregate state, and produces domain events.
- Event Store is an append-only log of domain events. Each aggregate (say, an Order) has its own event stream. This is the system of record.
- Aggregate is a domain object that enforces business invariants. It loads its state by replaying events from the event store, then decides whether to accept or reject a command.
- Projector listens to the event stream and builds read-optimized projections (SQL tables, Elasticsearch indexes, Redis caches). Each projection is disposable and can be rebuilt by replaying all events.
- Read Model is the denormalized data store optimized for specific query patterns. You can have many read models, each serving a different use case.
Snapshotting
Replaying thousands of events to reconstruct an aggregate gets slow. Snapshotting fixes this: periodically save the aggregate's current state alongside the event sequence number. On load, start from the latest snapshot and replay only the events after that point. Snapshot every 100-500 events depending on how complex the aggregate is.
Projection Management
Projections are the operational heart of the system. A few practices that matter:
- Idempotent projectors. Projectors must handle duplicate events gracefully. Use the event's sequence number to detect and skip duplicates.
- Rebuild capability. Every projection must be rebuildable from scratch by replaying the event store. This is how you recover when projection logic has a bug.
- Monitor projection lag. Track the delay between event publication and projection update. Alert if lag exceeds your SLA (typically under 1 second for real-time views, under 1 minute for dashboards).
- Versioned projections. When projection logic changes, deploy the new projector alongside the old one, rebuild the new projection, then cut over.
When NOT to Use Event Sourcing
Event sourcing is a poor fit when your domain has no audit requirements, your events do not carry meaningful business semantics, your team lacks distributed systems experience, you need immediate consistency everywhere, or your data includes PII that must be deleted. GDPR right-to-erasure conflicts directly with immutable event stores, so you would need crypto-shredding or tombstone events to handle that.
Real-World Examples
- Banking and finance. Ledgers are naturally event-sourced. Every transaction is an event. Account balances are projections.
- E-commerce order management. OrderPlaced, PaymentReceived, ItemShipped, OrderDelivered. Each event triggers downstream projections for tracking, analytics, and customer notifications.
- Healthcare records. Patient history is a sequence of events (diagnoses, prescriptions, procedures). Temporal queries like "What medications was this patient on in March 2024?" become trivial with event sourcing.
- Inventory management. Stock movements (received, allocated, shipped, returned) as events. Multiple projections cover current stock levels, warehouse utilization, and reorder alerts.
Key Points
- •CQRS separates read and write models so each can be optimized independently. Write models enforce business invariants, read models are denormalized for query performance
- •Event sourcing stores state as a sequence of immutable events rather than mutable rows. This gives you a complete audit trail and enables temporal queries
- •CQRS and event sourcing are independent patterns. You can use CQRS without event sourcing, and event sourcing without CQRS, though they pair well together
- •Event sourcing makes your system's history a first-class citizen. You can reconstruct state at any point in time by replaying events up to that timestamp
- •Projection management is the hidden operational cost. Each read model requires a projector that processes events and updates the denormalized view
Common Mistakes
- ✗Applying CQRS/ES to every service. These patterns add real complexity and only pay off for domains with audit requirements, complex business rules, or temporal query needs
- ✗Using the event store as the source of truth for queries. The event store is the write model. Build separate read-optimized projections for queries
- ✗Designing events that are too granular. 'FieldXChanged' events create noise. Prefer domain events that capture business intent like 'OrderShipped'
- ✗Not planning for event schema evolution. Your event store will contain events written years ago, and you need upcasting strategies to handle those old formats
- ✗Ignoring projection lag. Read models are eventually consistent with the write model. If your domain requires immediate read-your-writes consistency, you need a synchronous path or causal consistency mechanisms