---
# === IDENTITY ===
id: software/system-design/cqrs-event-sourcing/2026
canonical_question: "How do I implement CQRS and Event Sourcing?"
aliases:
  - "CQRS event sourcing implementation guide"
  - "command query responsibility segregation pattern"
  - "event sourcing with separate read write models"
  - "CQRS architecture design"
  - "implement event store with projections"
  - "separate read and write side architecture"
  - "event-driven CQRS microservices"
  - "how to build an event sourced system"
entity_type: software_reference
domain: software > system-design > cqrs_event_sourcing
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-23
confidence: 0.91
freshness: quarterly
version: 1.0
first_published: 2026-02-23

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: null
  next_review: 2026-08-22
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Never use CQRS as a top-level architecture for the entire system — apply it selectively to bounded contexts that benefit from separate read/write models"
  - "Event schemas are your API contract — never modify published event schemas, only add new event types or version existing ones"
  - "Event stores must be append-only — never update or delete events in production; use compensating events instead"
  - "Projections must be rebuildable from scratch — never store data in read models that cannot be reconstructed from the event stream"
  - "Do not use CQRS without operational maturity (monitoring, tracing, dead-letter queues) — eventual consistency bugs are invisible without observability"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Application is a simple CRUD app with fewer than 5 entities and no audit requirements"
    use_instead: "Standard three-tier architecture with a single database"
  - condition: "User needs event-driven microservices communication but not event sourcing"
    use_instead: "Event-driven architecture with message broker (Kafka, RabbitMQ)"
  - condition: "User wants to decompose a monolith into services"
    use_instead: "software/migrations/monolith-to-microservices/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: scope
    question: "Do you need CQRS only (separate read/write models) or full CQRS + Event Sourcing (append-only event log as source of truth)?"
    type: choice
    options: ["CQRS only", "CQRS + Event Sourcing"]
  - key: scale
    question: "What is the expected concurrent user load?"
    type: choice
    options: ["<1K users", "1K-100K users", ">100K users"]
  - key: language
    question: "What is the primary programming language?"
    type: choice
    options: ["Python", "JavaScript/TypeScript", "Java/Kotlin", "C#/.NET", "Go"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/system-design/cqrs-event-sourcing/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-23)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "software/system-design/domain-driven-design/2026"
      label: "Domain-Driven Design Fundamentals"
  related_to:
    - id: "software/migrations/monolith-to-microservices/2026"
      label: "Monolith to Microservices Migration"
    - id: "software/system-design/event-driven-architecture/2026"
      label: "Event-Driven Architecture Patterns"
  solves:
    - id: "software/system-design/audit-logging/2026"
      label: "Audit Logging and Compliance"
  alternative_to:
    - id: "software/system-design/crud-architecture/2026"
      label: "Traditional CRUD Architecture"
  often_confused_with:
    - id: "software/system-design/event-driven-architecture/2026"
      label: "Event-Driven Architecture (EDA is broader; CQRS/ES is a specific pattern within EDA)"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "bliki: CQRS"
    author: Martin Fowler
    url: https://www.martinfowler.com/bliki/CQRS.html
    type: technical_blog
    published: 2011-07-14
    reliability: authoritative
  - id: src2
    title: "CQRS Documents"
    author: Greg Young
    url: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
    type: technical_blog
    published: 2010-11-01
    reliability: authoritative
  - id: src3
    title: "CQRS Pattern - Azure Architecture Center"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs
    type: official_docs
    published: 2023-11-15
    reliability: high
  - id: src4
    title: "Event Sourcing Pattern - Azure Architecture Center"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing
    type: official_docs
    published: 2023-11-15
    reliability: high
  - id: src5
    title: "Pattern: Event Sourcing"
    author: Chris Richardson
    url: https://microservices.io/patterns/data/event-sourcing.html
    type: technical_blog
    published: 2018-01-01
    reliability: high
  - id: src6
    title: "EventStoreDB Documentation"
    author: Event Store Ltd
    url: https://developers.eventstore.com/getting-started
    type: official_docs
    published: 2024-01-01
    reliability: high
  - id: src7
    title: "Event Sourcing, CQRS, Stream Processing and Apache Kafka"
    author: Confluent
    url: https://www.confluent.io/blog/event-sourcing-cqrs-stream-processing-apache-kafka-whats-connection/
    type: technical_blog
    published: 2023-03-01
    reliability: moderate_high
---

# CQRS and Event Sourcing: Implementation Guide

## TL;DR

- **Bottom line**: CQRS separates read and write models for independent optimization; Event Sourcing stores state as an append-only sequence of domain events, enabling complete audit trails and temporal queries.
- **Key tool/command**: `EventStoreDB` (purpose-built event store), or `Apache Kafka` with log compaction disabled for event sourcing on existing infrastructure.
- **Watch out for**: Applying CQRS/ES globally instead of selectively to bounded contexts that actually benefit from it — most systems do not need this complexity.
- **Works with**: Any language/framework; dedicated support in Axon Framework (Java), Marten (C#/.NET), EventStoreDB (polyglot), Kafka Streams, and custom implementations.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never use CQRS as a top-level architecture for the entire system — apply it selectively to bounded contexts that benefit from separate read/write models [src1]
- Event schemas are your API contract — never modify published event schemas; only add new event types or version existing ones [src2]
- Event stores must be append-only — never update or delete events in production; use compensating events instead [src4]
- Projections must be rebuildable from scratch — never store data in read models that cannot be reconstructed from the event stream [src6]
- Do not use CQRS without operational maturity (monitoring, tracing, dead-letter queues) — eventual consistency bugs are invisible without observability [src3]

## Quick Reference

| Component | Role | Technology Options | Scaling Strategy |
|---|---|---|---|
| Command API | Accepts write requests, validates business rules | REST/gRPC endpoint, message queue consumer | Horizontal scaling behind load balancer |
| Command Handler | Processes commands, loads aggregate, emits events | Application service layer | Stateless; scale by adding instances |
| Aggregate | Enforces business invariants, produces domain events | Domain model (DDD aggregate root) | One aggregate instance per ID (single-writer) |
| Event Store | Append-only persistence of domain events | EventStoreDB, PostgreSQL + outbox, Kafka, DynamoDB | Partition by aggregate ID / stream |
| Event Bus | Distributes events to subscribers | Kafka, RabbitMQ, Amazon SNS/SQS, Azure Service Bus | Partition by event type or aggregate ID |
| Projection Engine | Transforms events into read-optimized views | Kafka Streams, custom subscribers, Axon projections | One consumer group per projection |
| Read Model Store | Serves denormalized query data | PostgreSQL, Redis, Elasticsearch, MongoDB | Read replicas, caching, sharding |
| Query API | Serves read requests from materialized views | REST/GraphQL endpoint | Horizontal scaling + CDN/cache |
| Snapshot Store | Caches aggregate state at intervals to speed rebuilds | Same DB as event store or separate cache | Snapshot every N events (100-500) |
| Saga / Process Manager | Coordinates multi-aggregate workflows | Stateful orchestrator consuming events | Partition by saga ID |
| Dead Letter Queue | Captures failed event processing for retry | Kafka DLQ, SQS DLQ, RabbitMQ dead-letter exchange | Monitor size; alert on growth |
| Schema Registry | Manages event schema evolution and compatibility | Confluent Schema Registry, AWS Glue, custom | Central service; version all schemas |

## Decision Tree

```
START
├── Do you need different read and write models?
│   ├── NO → Standard CRUD is simpler; skip CQRS entirely
│   └── YES ↓
├── Do you need a complete audit trail / temporal queries?
│   ├── NO → Use CQRS only (separate read/write models, shared or separate DB)
│   │         See "CQRS Only" approach below
│   └── YES ↓
├── CQRS + Event Sourcing
│   ├── Expected load < 1K concurrent users?
│   │   ├── YES → Single-node EventStoreDB or PostgreSQL event table
│   │   │         Synchronous projections acceptable
│   │   └── NO ↓
│   ├── Expected load 1K-100K concurrent users?
│   │   ├── YES → EventStoreDB cluster or Kafka + dedicated projection service
│   │   │         Async projections with consumer groups
│   │   └── NO ↓
│   └── Expected load > 100K concurrent users?
│       └── YES → Kafka event backbone + partitioned read stores
│                 (Elasticsearch, Redis, Cassandra)
│                 Multiple projection services + snapshotting
└── Framework available for your language?
    ├── Java/Kotlin → Axon Framework (batteries-included)
    ├── C#/.NET → Marten or EventStoreDB .NET client
    ├── Python/JS/Go → Custom implementation with EventStoreDB or Kafka
    └── Any → EventStoreDB gRPC client (polyglot)
```

## Step-by-Step Guide

### 1. Define bounded context and aggregates

Identify the domain boundary where CQRS/ES adds value. Map aggregates (consistency boundaries) using Domain-Driven Design. Each aggregate will have its own event stream. [src1]

```
Example bounded context: Order Management
├── Aggregate: Order (OrderId)
│   Events: OrderCreated, ItemAdded, ItemRemoved, OrderConfirmed, OrderShipped
├── Aggregate: Inventory (ProductId)
│   Events: StockReserved, StockReleased, StockDepleted
└── Process Manager: OrderFulfillment
    Listens: OrderConfirmed → reserves stock → emits OrderReadyToShip
```

**Verify**: Each aggregate has a clear consistency boundary. No two aggregates share the same invariant rules.

### 2. Design event schemas

Define immutable event types with all data needed to reconstruct state. Use past-tense naming. Include metadata (event ID, timestamp, aggregate ID, version). [src2]

```python
# Event schema example
event = {
    "event_type": "OrderCreated",
    "event_id": "uuid-v4",
    "aggregate_id": "order-123",
    "aggregate_type": "Order",
    "version": 1,
    "timestamp": "2026-02-23T10:00:00Z",
    "data": {
        "customer_id": "cust-456",
        "items": [{"product_id": "prod-789", "quantity": 2, "price": 29.99}],
        "total": 59.98
    },
    "metadata": {
        "correlation_id": "req-abc",
        "causation_id": None,
        "user_id": "user-001"
    }
}
```

**Verify**: Every event contains enough data to rebuild aggregate state without external lookups.

### 3. Implement the event store

Choose your storage backend. The event store must support append-only writes, optimistic concurrency control (expected version), and reading all events for a given aggregate. [src6]

```sql
-- PostgreSQL event store table
CREATE TABLE events (
    event_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    aggregate_id   UUID NOT NULL,
    aggregate_type VARCHAR(100) NOT NULL,
    event_type     VARCHAR(100) NOT NULL,
    version        INTEGER NOT NULL,
    data           JSONB NOT NULL,
    metadata       JSONB DEFAULT '{}',
    created_at     TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE (aggregate_id, version)  -- optimistic concurrency
);

CREATE INDEX idx_events_aggregate ON events (aggregate_id, version);
CREATE INDEX idx_events_type ON events (event_type, created_at);
```

**Verify**: `INSERT` with duplicate `(aggregate_id, version)` raises a unique constraint violation (concurrency conflict).

### 4. Build the command side (write model)

Implement command handlers that load an aggregate from events, apply business rules, and append new events. Use optimistic concurrency to prevent conflicting writes. [src2]

```python
# Command handler pattern
def handle_confirm_order(command):
    # 1. Load events from store
    events = event_store.load(command.order_id)
    # 2. Rebuild aggregate state
    order = Order()
    for event in events:
        order.apply(event)
    # 3. Execute business logic
    new_events = order.confirm()  # raises if invalid state
    # 4. Persist new events (with expected version for concurrency)
    event_store.append(
        aggregate_id=command.order_id,
        events=new_events,
        expected_version=order.version
    )
    # 5. Publish events to bus
    event_bus.publish(new_events)
```

**Verify**: Sending the same command twice with the same expected version raises a concurrency error on the second attempt.

### 5. Build projections (read model)

Create event handlers that subscribe to events and update denormalized read models optimized for specific queries. Each projection is independent and rebuildable. [src3]

```javascript
// Projection: Order Summary (denormalized for dashboard queries)
async function handleEvent(event) {
    switch (event.event_type) {
        case 'OrderCreated':
            await db.query(
                `INSERT INTO order_summary (order_id, customer_id, status, total, created_at)
                 VALUES ($1, $2, 'pending', $3, $4)`,
                [event.aggregate_id, event.data.customer_id,
                 event.data.total, event.timestamp]
            );
            break;
        case 'OrderConfirmed':
            await db.query(
                `UPDATE order_summary SET status = 'confirmed', confirmed_at = $2
                 WHERE order_id = $1`,
                [event.aggregate_id, event.timestamp]
            );
            break;
        case 'OrderShipped':
            await db.query(
                `UPDATE order_summary SET status = 'shipped', shipped_at = $2
                 WHERE order_id = $1`,
                [event.aggregate_id, event.timestamp]
            );
            break;
    }
}
```

**Verify**: After publishing events, query the read model and confirm the projected data matches expected state.

### 6. Handle eventual consistency in the UI

The read model lags behind the write model. Implement strategies to handle this: return the command result with the new version, poll until the projection catches up, or use client-side optimistic updates. [src3]

```
Strategies for eventual consistency:
1. Return write version → client polls read model until version >= write version
2. Causal consistency token → pass correlation ID, read model waits for that event
3. Optimistic UI → update client state immediately, reconcile when projection arrives
4. Read-your-writes → route reads to write model for N seconds after a command
```

**Verify**: After a write, the UI shows updated data within the acceptable latency window (typically < 500ms for interactive UIs).

### 7. Add snapshotting for large aggregates

When aggregates accumulate hundreds of events, replaying from the beginning becomes slow. Snapshot aggregate state periodically and load from the latest snapshot plus subsequent events. [src6]

```python
def load_aggregate(aggregate_id):
    # Try loading from snapshot first
    snapshot = snapshot_store.get_latest(aggregate_id)
    if snapshot:
        aggregate = deserialize(snapshot.state)
        # Load only events after snapshot
        events = event_store.load(
            aggregate_id,
            from_version=snapshot.version + 1
        )
    else:
        aggregate = Order()
        events = event_store.load(aggregate_id)
    for event in events:
        aggregate.apply(event)
    return aggregate

def save_snapshot(aggregate, every_n=100):
    if aggregate.version % every_n == 0:
        snapshot_store.save(
            aggregate_id=aggregate.id,
            version=aggregate.version,
            state=serialize(aggregate)
        )
```

**Verify**: Load time for an aggregate with 10,000 events + snapshotting should be < 50ms (vs. seconds without).

## Code Examples

### Python: Simple Event Store with Optimistic Concurrency

```python
# Input:  Domain events as dictionaries with aggregate_id and version
# Output: Persisted events with concurrency protection
import json
import uuid
from datetime import datetime, timezone
import psycopg2
from psycopg2.extras import RealDictCursor

class EventStore:
    """Append-only event store with optimistic concurrency control."""

    def __init__(self, dsn: str):
        self.conn = psycopg2.connect(dsn)

    def append(self, aggregate_id: str, events: list[dict],
               expected_version: int) -> None:
        """Append events. Raises on version conflict."""
        with self.conn.cursor() as cur:
            for i, event in enumerate(events):
                version = expected_version + i + 1
                cur.execute(
                    """INSERT INTO events
                       (event_id, aggregate_id, aggregate_type,
                        event_type, version, data, metadata)
                       VALUES (%s, %s, %s, %s, %s, %s, %s)""",
                    (str(uuid.uuid4()), aggregate_id,
                     event["aggregate_type"], event["event_type"],
                     version, json.dumps(event["data"]),
                     json.dumps(event.get("metadata", {})))
                )
        self.conn.commit()

    def load(self, aggregate_id: str,
             from_version: int = 0) -> list[dict]:
        """Load events for an aggregate, optionally from a version."""
        with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute(
                """SELECT * FROM events
                   WHERE aggregate_id = %s AND version > %s
                   ORDER BY version""",
                (aggregate_id, from_version)
            )
            return cur.fetchall()

    def load_all(self, from_position: int = 0,
                 batch_size: int = 1000) -> list[dict]:
        """Load all events globally (for projections)."""
        with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute(
                """SELECT * FROM events
                   WHERE event_id > %s
                   ORDER BY created_at
                   LIMIT %s""",
                (from_position, batch_size)
            )
            return cur.fetchall()


class Order:
    """Event-sourced aggregate example."""

    def __init__(self):
        self.id = None
        self.status = None
        self.items = []
        self.total = 0.0
        self.version = 0
        self._pending_events = []

    def apply(self, event: dict) -> None:
        """Replay a historical event to rebuild state."""
        handler = getattr(self, f"_on_{event['event_type']}", None)
        if handler:
            handler(event["data"])
        self.version = event["version"]

    def create(self, order_id: str, customer_id: str,
               items: list) -> list[dict]:
        """Command: create a new order."""
        if self.status is not None:
            raise ValueError("Order already exists")
        total = sum(i["price"] * i["quantity"] for i in items)
        event = {
            "aggregate_type": "Order",
            "event_type": "OrderCreated",
            "data": {"customer_id": customer_id,
                     "items": items, "total": total}
        }
        self._apply_new(event, {"order_id": order_id})
        return self._pending_events

    def confirm(self) -> list[dict]:
        """Command: confirm the order."""
        if self.status != "pending":
            raise ValueError(f"Cannot confirm order in '{self.status}' state")
        event = {
            "aggregate_type": "Order",
            "event_type": "OrderConfirmed",
            "data": {"confirmed_at": datetime.now(timezone.utc).isoformat()}
        }
        self._apply_new(event)
        return self._pending_events

    def _apply_new(self, event: dict, extra_data: dict = None) -> None:
        if extra_data:
            event["data"].update(extra_data)
        fake_event = {**event, "version": self.version + 1}
        self.apply(fake_event)
        self._pending_events.append(event)

    def _on_OrderCreated(self, data: dict) -> None:
        self.id = data.get("order_id")
        self.status = "pending"
        self.items = data["items"]
        self.total = data["total"]

    def _on_OrderConfirmed(self, data: dict) -> None:
        self.status = "confirmed"
```

### Node.js: Read Model Projection with Event Subscription

```javascript
// Input:  Events from an event store (EventStoreDB or Kafka)
// Output: Denormalized read model in PostgreSQL
const { Pool } = require('pg');  // pg@8.x
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Projection: maintains a denormalized order_summary table
const projectionHandlers = {
  async OrderCreated(event) {
    const { customer_id, items, total } = event.data;
    await pool.query(
      `INSERT INTO order_summary
       (order_id, customer_id, status, item_count, total, created_at, updated_at)
       VALUES ($1, $2, 'pending', $3, $4, $5, $5)
       ON CONFLICT (order_id) DO NOTHING`,
      [event.aggregate_id, customer_id, items.length, total, event.timestamp]
    );
  },

  async OrderConfirmed(event) {
    await pool.query(
      `UPDATE order_summary
       SET status = 'confirmed', updated_at = $2
       WHERE order_id = $1`,
      [event.aggregate_id, event.timestamp]
    );
  },

  async OrderShipped(event) {
    const { tracking_number, carrier } = event.data;
    await pool.query(
      `UPDATE order_summary
       SET status = 'shipped', tracking_number = $2,
           carrier = $3, updated_at = $4
       WHERE order_id = $1`,
      [event.aggregate_id, tracking_number, carrier, event.timestamp]
    );
  },

  async OrderCancelled(event) {
    await pool.query(
      `UPDATE order_summary
       SET status = 'cancelled', updated_at = $2
       WHERE order_id = $1`,
      [event.aggregate_id, event.timestamp]
    );
  }
};

// Projection runner with checkpoint tracking
async function runProjection(eventStore, projectionName) {
  // Load last processed position
  const checkpoint = await loadCheckpoint(projectionName);
  let position = checkpoint;
  console.log(`Starting projection '${projectionName}' from position ${position}`);

  // Subscribe to all events from last checkpoint
  const stream = eventStore.subscribeToAll({ fromPosition: position });
  for await (const resolvedEvent of stream) {
    const event = resolvedEvent.event;
    const handler = projectionHandlers[event.event_type];
    if (handler) {
      try {
        await handler(event);
        await saveCheckpoint(projectionName, resolvedEvent.position);
        position = resolvedEvent.position;
      } catch (err) {
        console.error(`Projection error at ${resolvedEvent.position}:`, err);
        // Send to dead letter queue for manual inspection
        await sendToDeadLetter(projectionName, resolvedEvent, err);
      }
    }
  }
}

async function loadCheckpoint(name) {
  const result = await pool.query(
    'SELECT position FROM projection_checkpoints WHERE name = $1',
    [name]
  );
  return result.rows[0]?.position || 0;
}

async function saveCheckpoint(name, position) {
  await pool.query(
    `INSERT INTO projection_checkpoints (name, position, updated_at)
     VALUES ($1, $2, NOW())
     ON CONFLICT (name) DO UPDATE SET position = $2, updated_at = NOW()`,
    [name, position]
  );
}

async function sendToDeadLetter(projection, event, error) {
  await pool.query(
    `INSERT INTO dead_letter_queue (projection, event_id, event_data, error, created_at)
     VALUES ($1, $2, $3, $4, NOW())`,
    [projection, event.event?.event_id, JSON.stringify(event), error.message]
  );
}

module.exports = { runProjection, projectionHandlers };
```

## Anti-Patterns

### Wrong: Mutable events in the store

```python
# BAD — modifying events after they are stored destroys the audit trail
def fix_order_total(order_id, correct_total):
    event = event_store.load_latest(order_id, "OrderCreated")
    event["data"]["total"] = correct_total  # MUTATING a stored event
    event_store.update(event)               # UPDATING instead of appending
```

### Correct: Use compensating events

```python
# GOOD — append a correction event; the original event remains immutable
def fix_order_total(order_id, correct_total, reason):
    event_store.append(order_id, [{
        "aggregate_type": "Order",
        "event_type": "OrderTotalCorrected",
        "data": {
            "previous_total": order.total,
            "corrected_total": correct_total,
            "reason": reason
        }
    }], expected_version=order.version)
```

### Wrong: One giant read model serving all queries

```sql
-- BAD — single normalized read model defeats the purpose of CQRS
SELECT o.id, o.status, c.name, c.email,
       SUM(oi.price * oi.quantity) as total,
       s.tracking_number, s.carrier
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.id = oi.order_id
LEFT JOIN shipments s ON o.id = s.order_id
WHERE o.status = 'shipped'
GROUP BY o.id, o.status, c.name, c.email, s.tracking_number, s.carrier;
```

### Correct: Purpose-built denormalized projections

```sql
-- GOOD — each projection is a flat, denormalized table for one use case
-- Projection: "shipped orders dashboard"
CREATE TABLE shipped_orders_view (
    order_id UUID PRIMARY KEY,
    customer_name TEXT,
    customer_email TEXT,
    item_count INTEGER,
    total DECIMAL(10,2),
    tracking_number TEXT,
    carrier TEXT,
    shipped_at TIMESTAMPTZ
);
-- Single-row lookup, no JOINs, no GROUP BY
SELECT * FROM shipped_orders_view WHERE order_id = $1;
```

### Wrong: Exposing domain events to external services directly

```python
# BAD — internal domain events leak aggregate internals to consumers
event_bus.publish_to_external("order-events-topic", domain_event)
# External consumers now depend on internal event schemas
# Changing internal events breaks all consumers
```

### Correct: Translate domain events to integration events

```python
# GOOD — publish stable integration events for external consumers
def on_order_confirmed(domain_event):
    integration_event = {
        "type": "OrderConfirmedV1",
        "order_id": domain_event.data["order_id"],
        "total": domain_event.data["total"],
        "confirmed_at": domain_event.timestamp
        # Only expose what external consumers need
    }
    integration_bus.publish("public.orders", integration_event)
```

### Wrong: Validating commands against the read model

```python
# BAD — read model may be stale; business rules must use the write model
async def handle_confirm_order(command):
    order_summary = await read_db.query(
        "SELECT status FROM order_summary WHERE order_id = $1",
        command.order_id
    )
    if order_summary.status == "pending":  # STALE data!
        event_store.append(...)
```

### Correct: Load aggregate from events for command validation

```python
# GOOD — always load from the event store for write-side decisions
async def handle_confirm_order(command):
    events = event_store.load(command.order_id)
    order = Order()
    for event in events:
        order.apply(event)
    # Business rules enforced against consistent aggregate state
    new_events = order.confirm()  # raises if not in valid state
    event_store.append(command.order_id, new_events, order.version)
```

## Common Pitfalls

- **Eventual consistency surprises**: Users submit a command, then immediately query the read model and see stale data. Fix: `implement read-your-writes consistency via polling with version check or causal consistency tokens`. [src3]
- **Event schema evolution breaks projections**: Adding/removing fields in events without versioning causes deserialization failures in existing projections. Fix: `use a schema registry (Confluent Schema Registry, Avro, Protobuf) with backward-compatible changes only`. [src7]
- **Unbounded event streams per aggregate**: Aggregates with millions of events (e.g., a "global counter") become impossibly slow to rebuild. Fix: `snapshot every 100-500 events, or redesign the aggregate boundary to limit event count per stream`. [src6]
- **Projection replay takes hours**: Rebuilding a projection from scratch is slow when the event store has millions of events. Fix: `use parallel projection with partitioned consumers, pre-filtered event streams, and checkpoint-based resumption`. [src7]
- **Missing idempotency in projections**: Network retries cause duplicate event delivery, leading to double-counted read model data. Fix: `track last processed event ID per projection (projection_checkpoints table) and skip duplicates`. [src5]
- **Treating CQRS as all-or-nothing**: Trying to apply CQRS + ES to the entire application instead of specific bounded contexts. Fix: `start with one complex domain that genuinely benefits, keep the rest as simple CRUD`. [src1]
- **No dead letter queue for failed projections**: A single bad event halts the entire projection pipeline. Fix: `route failed events to a dead-letter queue, alert on queue depth, and continue processing`. [src4]
- **Synchronous projections in the command path**: Updating projections inside the command handler creates tight coupling and slows writes. Fix: `project asynchronously via event subscriptions; accept eventual consistency as a feature`. [src2]

## Diagnostic Commands

```bash
# Check EventStoreDB health (Docker)
curl -s http://localhost:2113/health/live | jq .

# Count events per aggregate in PostgreSQL event store
psql -c "SELECT aggregate_id, COUNT(*) as event_count, MAX(version) as latest_version FROM events GROUP BY aggregate_id ORDER BY event_count DESC LIMIT 20;"

# Check projection lag (difference between latest event and last projected)
psql -c "SELECT e.max_id AS latest_event, p.position AS projected_to, (e.max_id - p.position) AS lag FROM (SELECT MAX(event_id) as max_id FROM events) e, projection_checkpoints p WHERE p.name = 'order_summary';"

# Monitor dead letter queue depth
psql -c "SELECT projection, COUNT(*) as failed_count, MIN(created_at) as oldest_failure FROM dead_letter_queue GROUP BY projection;"

# Verify event store append-only constraint
psql -c "SELECT COUNT(*) FROM events WHERE created_at > NOW() - INTERVAL '1 hour';"

# Check EventStoreDB stream stats
curl -s http://localhost:2113/streams/\$stats-0 -H "Accept: application/json" | jq .
```

## Version History & Compatibility

| Pattern / Tool | Status | Breaking Changes | Notes |
|---|---|---|---|
| CQRS (pattern) | Stable since 2010 | None — architectural pattern | Coined by Greg Young, based on CQS (Bertrand Meyer) |
| Event Sourcing (pattern) | Stable since 2005 | None — architectural pattern | Popularized by Greg Young and Martin Fowler |
| EventStoreDB 24.x | Current | gRPC API is primary (HTTP deprecated) | Docker: `eventstore/eventstore:24.10` |
| EventStoreDB 23.x | Maintained | — | Last version with full HTTP API |
| Axon Framework 4.x | Current (LTS) | Axon Server required for clustering | Java 17+ required since 4.8 |
| Marten 7.x | Current | PostgreSQL 12+ required | .NET 8+ required |
| Kafka (as event store) | Usable with caveats | Not a true event store (no per-stream reads) | Use log compaction OFF for event streams |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Read and write workloads have vastly different scaling requirements | Simple CRUD with balanced read/write load | Standard three-tier architecture |
| You need a complete, immutable audit trail of all state changes | Audit requirements are satisfied by database change logs | CDC (Change Data Capture) with Debezium |
| Domain has complex business rules that benefit from DDD aggregates | Domain is anemic with simple property updates | Active Record or Repository pattern |
| Multiple read models are needed for different query patterns (dashboard, search, reports) | Single query pattern serves all use cases | Standard database views or materialized views |
| You need temporal queries ("what was the state at time T?") | Only current state matters | Standard database with updated_at timestamps |
| High-throughput write workload that benefits from append-only storage | Write volume is moderate and relational ACID is sufficient | PostgreSQL with proper indexing |
| Microservices need to react to domain events across boundaries | Monolithic application with shared database | Database triggers or application events |

## Important Caveats

- CQRS and Event Sourcing are independent patterns that complement each other but do not require each other. You can use CQRS with a traditional database, or event sourcing without CQRS (though this is rare and often impractical). [src1]
- Martin Fowler explicitly warns: "the majority of cases I've run into have not been so good, with CQRS seen as a significant force for getting a software system into serious difficulties." Only apply where complexity is justified. [src1]
- Eventual consistency between write and read models is inherent. There will always be a window (typically milliseconds to seconds) where the read model is behind. Design your UX around this reality. [src3]
- Event versioning and schema evolution become critical at scale. Plan for upcasting (transforming old event formats to new ones during replay) from day one. [src2]
- GDPR and data deletion requirements conflict with immutable event stores. Solutions include crypto-shredding (encrypting PII with per-user keys, then deleting the key) or event store compaction with redaction. [src4]

## Related Units

- [Domain-Driven Design Fundamentals](/software/system-design/domain-driven-design/2026)
- [Monolith to Microservices Migration](/software/migrations/monolith-to-microservices/2026)
- [Event-Driven Architecture Patterns](/software/system-design/event-driven-architecture/2026)
