CQRS and Event Sourcing Explained — with Real-World Examples
This article is a deep dive into Command Query Responsibility Segregation (CQRS) and Event Sourcing: history, concepts, architecture, design patterns, implementation examples, operational concerns, trade-offs, and practical advice. It targets architects, backend engineers, and technical leads who want to understand when and how to apply these patterns in production systems.
Table of contents
- Overview and history
- Key concepts and terminology
- Theoretical foundations and motivations
- Architecture and components
- Event design and modeling
- Example domains and real-world scenarios
- Implementation patterns and code examples
- Operational concerns and tooling
- Versioning, migration and schema evolution
- When to use — and when not to use — CQRS + Event Sourcing
- Future trends and concluding guidance
- References and further reading
Overview and history
CQRS and Event Sourcing are related but distinct architectural patterns. Both gained prominence through Domain-Driven Design (DDD) practices and the work of practitioners like Greg Young (who popularized Event Sourcing and CQRS), Udi Dahan, and others. Historically, systems used a single model for reads and writes (CRUD). As systems grew in complexity—requiring scalability, auditability, business workflows, and distributed processing—CQRS and Event Sourcing emerged as ways to decouple concerns and capture domain intent more precisely.
- CQRS: separates the write model (commands) from the read model (queries). The read model is optimized for querying and often denormalized.
- Event Sourcing: persists state changes as a sequence of immutable events rather than storing the current state.
Together, they form a powerful approach for complex domains: writes produce events that mutate the event store; projections consume events to build read-optimized views.
Key concepts and terminology
- Command: an imperative request to perform an action (e.g., "PlaceOrder", "WithdrawFunds"). Commands are intent from an actor.
- Event: a record of something that happened in the past and is immutable (e.g., "OrderPlaced", "FundsWithdrawn").
- Aggregate: a DDD concept representing a consistency boundary; aggregates handle commands and produce events.
- Event Store: append-only log that persists sequences of events for aggregates. It’s the source of truth in Event Sourcing.
- Projection (Read Model): a view built by processing events to produce query-optimized data (often denormalized).
- Projection Store / Read Database: DB used for queries (could be SQL, NoSQL, Elasticsearch).
- Snapshot: a saved materialized state at a point to speed up reconstitution (replay) of aggregates.
- Saga / Process Manager: coordinates long-running transactions and interactions between services, often by handling events and issuing commands.
- Upcasting: migrating stored events to a newer schema at read time.
- Event Stream: sequence of events for a specific aggregate ID, often appended with ordering.
- Optimistic Concurrency Control: compare expected version when appending events to prevent races.
Theoretical foundations and motivations
Why would you use these patterns? Primary motivations:
- Auditability and Traceability
- Events are an immutable history of changes. Every change is recorded with context: who, when, why.
- Correctness & Domain Modeling
- Events capture domain intent and business invariants explicitly, enabling reasoning about domain behavior.
- Scalability & Performance
- By separating write and read models, each can be scaled and optimized differently.
- Flexibility
- New read models or projections can be created from the event stream without changing how data was written.
- Resilience & Integration
- Events can be published to other systems to react to changes (integration events).
- Temporal Queries and Replays
- You can replay events to reconstruct state at any point in time, perform analytics, audits, or bug fixes.
The trade-off: more complexity. You must handle eventual consistency, event schema evolution, idempotency, and operational tooling.
Architecture and components
High-level architecture when combining CQRS and Event Sourcing:
- Clients/UI -> issue Commands -> Command Handler
- Command Handler validates and loads Aggregate state by replaying events
- Aggregate executes business logic and emits new Events
- Events are appended to the Event Store (append-only)
- Event Store publishes events to one or more Event Bus(es)
- Projectors (or Projections) consume events and update Queryable Read Model(s)
- Read Model is queried by clients for queries (low latency, optimized)
Textual diagram:
- [Client] -> [Command API] -> [Command Handlers/Aggregates] -> [Event Store] -> [Event Bus] -> [Projectors/Read DBs] -> [Query API/Clients]
Important variants:
- Synchronous command handling that returns success/failure and maybe immediate read model state.
- Asynchronous projections and eventual consistency for reads.
- Multi-aggregate transactions implemented through Sagas/Process Managers that orchestrate via events/commands.
Event design and modeling
Good event design is essential. Events are domain facts, immutable and named in past tense. Keep these principles:
- Events are facts: "PaymentReceived", not "ProcessPayment".
- Keep event payloads minimal but sufficient: contain the data required to understand what changed and to rebuild state.
- Include metadata: timestamp, correlation id, causation id, user id, aggregate type, version.
- Design events for consumers: events are contracts; changing them can break consumers.
- Prefer creating new event types over mutating old ones—use upcasting or versioning strategies for backward compatibility.
- Ensure events are idempotent-aware and carry dedup tokens where necessary.
Event granularity:
- Fine-grained events capture low-level changes (e.g., "OrderItemQuantityChanged").
- Coarse-grained events capture larger domain actions (e.g., "OrderPlaced").
Choose granularity based on business needs and consumer requirements. Coarse-grained events are often easier to reason about; fine-grained can offer flexibility for projection rebuilding.
Event schemas:
- Use versioned schemas, and consider a schema registry (Avro, Protobuf, JSON Schema) for enforcement across teams.
Example domains and real-world scenarios
Below are several concrete examples showing how CQRS + Event Sourcing apply.
- E-commerce order lifecycle
- Commands: PlaceOrder, CancelOrder, ShipOrder
- Events: OrderPlaced, OrderCancelled, OrderPaid, OrderShipped, OrderItemAdded
- Read models: Order summary, customer order history, shipping dashboard, analytics
- Benefits: Auditable order history, easy projection for different UIs, compensation workflows (refunds) via sagas.
- Financial ledger / Banking (accounts)
- Commands: OpenAccount, Deposit, Withdraw, Transfer
- Events: AccountOpened, FundsDeposited, FundsWithdrawn, TransferInitiated, TransferCompleted
- Benefits: Strong audit trail, deterministic reconstruction, simple double-entry modeling using multiple streams, compliance.
- Inventory and supply chain
- Commands: ReserveStock, ReleaseReservation, AdjustInventory
- Events: StockReserved, StockReleased, InventoryAdjusted
- Benefits: Concurrent reservations via optimistic concurrency; projections for available stock, backorder lists.
- Collaborative editing and temporal queries
- Events capture every change. You can reconstruct document state at arbitrary times or implement time travel.
- IoT event streams and device telematics
- Device readings as events; projections for aggregated metrics and alerts.
Real-world companies and technologies:
- LinkedIn: heavy use of event streaming (Kafka) and CQRS-like patterns for feeds and metrics.
- Financial systems often use ledger-like event sourcing for auditability.
- EventStoreDB, Apache Kafka, and cloud services (AWS Kinesis, Azure Event Hubs) are commonly used.
Implementation patterns and code examples
Below are simplified examples to illustrate the core mechanics: aggregates, event store, projections, and optimistic concurrency. We'll show a minimal JavaScript/TypeScript example and then outline a C# style approach typically used in .NET ecosystems.
Minimal Node.js (TypeScript-like) example — Aggregate + In-memory Event Store
This is a didactic example for an Order aggregate. It is simplified and does not include persistence or retries.
```js // Simple in-memory event store class InMemoryEventStore { constructor() { this.streams = new Map(); // key: aggregateId -> events array }
async append(aggregateId, expectedVersion, events) { const stream = this.streams.get(aggregateId) || []; if (expectedVersion !== stream.length - 1 && expectedVersion !== null) { throw new Error('Concurrency conflict'); } for (const e of events) stream.push(e); this.streams.set(aggregateId, stream); return stream.length - 1; // return last version }
async load(aggregateId) { return this.streams.get(aggregateId) || []; } }
// Order aggregate class Order { constructor(id) { this.id = id; this.items = []; this.version = -1; this.uncommittedEvents = []; }
static async rehydrate(events, id) { const o = new Order(id); for (const e of events) { o.apply(e, false); o.version++; } return o; }
placeOrder(customerId, items) { if (!items || items.length === 0) throw new Error('Empty order'); const evt = { type: 'OrderPlaced', data: { orderId: this.id, customerId, items } }; this.apply(evt, true); }
apply(event, isNew) { switch (event.type) { case 'OrderPlaced': this.items = event.data.items; break; // other events... } if (isNew) this.uncommittedEvents.push(event); } }
// Command handler async function handlePlaceOrder(command, eventStore) { const events = await eventStore.load(command.orderId); const order = await Order.rehydrate(events, command.orderId); order.placeOrder(command.customerId, command.items); // append with optimistic concurrency - expectedVersion is current version await eventStore.append(order.id, order.version, order.uncommittedEvents); // publish events to event bus (omitted) } ```
Remarks:
- This demonstrates rehydration, uncommitted events, and optimistic concurrency.
- In production, replace in-memory store with persistent store (EventStoreDB, Kafka, RDBMS table append).
- Add metadata (timestamps, user id, correlation ids), and persist event envelopes.
C#/.NET conceptual example (pseudocode)
In .NET environments, the pattern is typically:
- Aggregate roots as classes that emit domain events.
- EventStoreDB or custom append-only tables.
- Projections implemented as event handlers updating read databases (SQL/NoSQL).
Pseudocode:
```csharp public class OrderAggregate : AggregateRoot { public void PlaceOrder(Guid orderId, Guid customerId, List items) { if (items.Count == 0) throw new DomainException("No items"); var ev = new OrderPlaced(orderId, customerId, items); ApplyChange(ev); }
protected void Apply(OrderPlaced e) { // update internal state } }
// Command handler public async Task Handle(PlaceOrder command) { ...