6 min read
On this page

Event Sourcing & CQRS

Overview

Event sourcing and CQRS are architectural patterns that separate how data is written from how it is read. They solve specific problems in complex domains but introduce significant operational complexity. Understanding when they help and when they hurt is critical.

Event Sourcing

Event sourcing stores the history of all state changes as an immutable sequence of events, rather than storing only the current state.

How It Works

Traditional approach (current state only):
  Account: { id: 123, balance: 750 }

Event sourcing approach (full history):
  Event 1: AccountOpened   { id: 123, initial_balance: 1000 }
  Event 2: MoneyWithdrawn  { id: 123, amount: 200 }
  Event 3: MoneyDeposited  { id: 123, amount: 150 }
  Event 4: MoneyWithdrawn  { id: 123, amount: 200 }

Current state is derived by replaying all events:
  1000 - 200 + 150 - 200 = 750

Core Principles

  • Events are immutable: Once written, events are never modified or deleted.
  • Events are the source of truth: The current state is derived, not stored directly.
  • Events capture intent: "MoneyWithdrawn" carries more meaning than "balance updated."
  • Events are ordered: The sequence matters. Replaying in a different order produces a different state.

Benefits

  • Complete audit trail: Every change is recorded with full context.
  • Temporal queries: "What was the account balance on March 15th?" is a replay to that point.
  • Debugging: Reproduce any bug by replaying the exact sequence of events.
  • Event-driven integration: Other services subscribe to events for their own processing.
  • Retroactive corrections: Apply new business logic to historical events.

Real-World Usage

Banking systems inherently use event sourcing. A ledger is a sequence of transactions (events). The balance is derived from the transaction history.

Git is event sourced. Each commit is an immutable event. The current state of the repository is derived by applying all commits in sequence.

Datomic (used by Nubank, one of the world's largest digital banks) stores all facts as immutable, timestamped events.

Event Store

The event store is the append-only database that holds all events. It is the heart of an event-sourced system.

Design Considerations

Event structure:
  {
    event_id:      "uuid-abc-123"
    aggregate_id:  "account-123"
    aggregate_type: "BankAccount"
    event_type:    "MoneyWithdrawn"
    event_data:    { amount: 200, reason: "ATM withdrawal" }
    metadata:      { user_id: "user-456", correlation_id: "req-789" }
    version:       4
    timestamp:     "2026-03-15T10:30:00Z"
  }

Storage requirements:
  - Append-only writes (events are never updated)
  - Ordered reads per aggregate (replay in sequence)
  - Optimistic concurrency (detect conflicting writes via version)
  - Efficient subscription (notify consumers of new events)

Implementation Options

  • EventStoreDB: Purpose-built event store with built-in projections and subscriptions.
  • PostgreSQL: Use an append-only events table with proper indexing. Works well for moderate scale.
  • Apache Kafka: Events as messages in a topic. Good for high throughput but lacks per-aggregate ordering guarantees without careful partitioning.
  • Amazon DynamoDB: Partition by aggregate ID, sort by version. Streams provide change notifications.

Projections

Projections (also called read models or views) are derived representations of events optimized for specific queries.

How Projections Work

Events (write side):
  OrderPlaced    { order_id: 1, customer_id: 42, items: [...] }
  PaymentReceived { order_id: 1, amount: 99.99 }
  OrderShipped   { order_id: 1, tracking: "UPS123" }

Projection 1 - Order Status (for customer):
  { order_id: 1, status: "shipped", tracking: "UPS123" }

Projection 2 - Revenue Report (for finance):
  { date: "2026-03-15", total_revenue: 99.99, order_count: 1 }

Projection 3 - Shipping Queue (for warehouse):
  [no entries - order already shipped]

Each projection processes the same events but builds a different
view optimized for its specific consumer.

Projection Strategies

  • Synchronous projections: Updated in the same transaction as the event write. Consistent but slower writes.
  • Asynchronous projections: Updated by a background process reading the event stream. Faster writes but read model may lag behind.
  • Catch-up subscriptions: Projections can be rebuilt from scratch by replaying all events from the beginning.

Snapshots

As the event history grows, replaying all events to rebuild state becomes slow. Snapshots periodically capture the current derived state.

Without snapshots:
  Replay events 1 through 10,000 to get current state.
  Time: seconds to minutes depending on complexity.

With snapshots:
  Load snapshot at event 9,950 (state at that point).
  Replay only events 9,951 through 10,000.
  Time: milliseconds.

Snapshot strategy:
  - Take a snapshot every N events (e.g., every 100)
  - Take a snapshot when replay time exceeds a threshold
  - Store snapshots alongside events, keyed by aggregate ID and version
  - Snapshots are an optimization, not the source of truth
  - The system must work correctly without snapshots (just slower)

CQRS

Command Query Responsibility Segregation separates the write model (commands) from the read model (queries) into distinct paths, potentially with different data stores.

How It Works

Traditional architecture:
  [Application] -> [Single Database] -> [Same tables for reads and writes]

CQRS architecture:
  Commands (writes):
    [Application] -> [Command Handler] -> [Write Store]
                                              |
                                        [Event/Change published]
                                              |
  Queries (reads):                            v
    [Application] -> [Query Handler] -> [Read Store(s)]

The write store is optimized for consistency and validation.
The read store is optimized for query performance.

Benefits of CQRS

  • Independent scaling: Scale read and write paths separately. Most systems are read-heavy.
  • Optimized data models: The write model enforces business rules. The read model is shaped for UI needs.
  • Simpler models: Each side handles one responsibility instead of compromising between reads and writes.
  • Performance: Read stores can be denormalized, pre-aggregated, and cached aggressively.

CQRS Without Event Sourcing

CQRS and event sourcing are often discussed together but are independent patterns.

CQRS without event sourcing:
  Write side: Normalized relational database with ACID transactions
  Read side: Denormalized views, search indexes, or caches
  Sync: Database triggers, change data capture, or application-level events

This is actually very common:
  - Write to PostgreSQL
  - Sync changes to Elasticsearch for search
  - Sync changes to Redis for caching
  - This is CQRS, even if nobody calls it that

Event Sourcing with CQRS

When combined, event sourcing provides the write model and events automatically feed projections that serve as read models.

Combined architecture:
  Command -> Validate -> Append event to event store
                              |
                         [Event bus]
                        /     |     \
                       v      v      v
                   [Read    [Read   [Read
                    Model    Model   Model
                    1]       2]      3]

  Query -> Read from appropriate read model

Example: E-commerce order
  Write: Append OrderPlaced event to event store
  Read Model 1: Customer order history (DynamoDB)
  Read Model 2: Product sales analytics (ClickHouse)
  Read Model 3: Warehouse fulfillment queue (Redis)

When to Use Event Sourcing

Good Fit

  • Audit-critical domains: Finance, healthcare, legal systems where you must prove what happened and when.
  • Complex business processes: Order fulfillment, insurance claims, loan processing with many state transitions.
  • Temporal queries: Systems that need to answer "what was the state at time X?"
  • Event-driven architectures: When downstream systems need to react to business events.
  • Collaborative editing: Systems where multiple users modify shared state concurrently.

Real Examples

Stripe uses event sourcing for payment processing. Every state change to a payment is recorded as an event, enabling complete audit trails and dispute resolution.

The New York Times uses event sourcing for their content publishing pipeline. Every edit to an article is an event, enabling full revision history.

When NOT to Use Event Sourcing

Poor Fit

  • Simple CRUD applications: A basic blog or content management system does not benefit from the complexity.
  • Reporting-first systems: If the primary need is ad-hoc queries across all data, event sourcing adds overhead without clear benefit.
  • Small teams: The operational burden of event stores, projections, and eventual consistency requires significant expertise.
  • Systems with frequent schema changes: Evolving event schemas while maintaining backward compatibility with historical events is hard.
Signs you should NOT use event sourcing:
  - Your domain events are just "EntityUpdated" with a diff
  - You cannot name meaningful domain events
  - Your team has no experience with eventual consistency
  - You are building a prototype or MVP
  - The data has no audit or temporal requirements

When to Use CQRS

Good Fit

  • Read and write workloads have very different scaling needs
  • Different consumers need different views of the same data
  • You already have separate read replicas or search indexes
  • Write validation logic is complex and different from query logic

Poor Fit

  • Simple domains where reads and writes use the same model
  • Systems where eventual consistency between read and write models is unacceptable
  • Small applications where the added infrastructure is not justified

Common Pitfalls

  • Using event sourcing for everything: It is a specialized pattern. Most services in your system should use straightforward CRUD.
  • Events that are just state dumps: If your events look like "UserUpdated { full user object }", you are not capturing intent. Real events are "EmailChanged" or "AddressUpdated."
  • Ignoring event schema evolution: Events are stored forever. You must handle old event formats alongside new ones, indefinitely.
  • Forgetting that projections are eventually consistent: The read model may lag behind the write model. Design your UI to handle this gracefully.
  • Not implementing snapshots: Without snapshots, aggregates with long histories become increasingly slow to load.
  • Overcomplicating CQRS: You do not need a message bus and separate databases to do CQRS. Reading from a database view while writing to normalized tables is CQRS.

Key Takeaways

  • Event sourcing stores all state changes as immutable events. The current state is derived by replaying the event history.
  • CQRS separates read and write models, allowing each to be independently optimized and scaled.
  • Event sourcing and CQRS are independent patterns that complement each other but neither requires the other.
  • Use event sourcing when audit trails, temporal queries, or complex domain events are requirements, not just nice-to-haves.
  • Most systems should not use event sourcing. The operational complexity is significant and only justified for specific problem domains.
  • CQRS is more broadly applicable. If you have separate read replicas or search indexes, you are already doing a form of CQRS.
  • Start simple. You can introduce these patterns incrementally for the parts of your system that need them.