Skip to content
Home
CQRS and Event Sourcing: Patterns for Scalable Systems

CQRS and Event Sourcing: Patterns for Scalable Systems

Software Design Software Design 7 min read 1475 words Beginner ExcellentWiki Editorial Team

CQRS (Command Query Responsibility Segregation) and Event Sourcing are architectural patterns that address common challenges in complex, high-scale systems. They are often used together but are independent patterns. CQRS separates read and write operations into different models. Event Sourcing stores state as a sequence of events rather than a snapshot. Together, they enable powerful capabilities: audit trails, temporal queries, scalable read models, and event-driven architectures.

CQRS Fundamentals

Traditional CRUD uses the same model for reads and writes. This works for simple applications but breaks down as complexity grows. Read and write operations have different shapes, performance characteristics, and scaling requirements.

Command-Query Separation

The CQRS principle is simple:

  • Commands change state. They return no data (or only confirmation). They express intent: PlaceOrder, UpdateShippingAddress, CancelSubscription.
  • Queries return data. They do not change state: GetOrderDetails, FindCustomerOrders.
// Command — changes state, no return value
public class PlaceOrderCommand {
    public CustomerId CustomerId { get; }
    public List<OrderLine> Lines { get; }
---

// Query — returns data, no side effects
public class GetOrderDetailsQuery {
    public OrderId OrderId { get; }
---

public class OrderDetails {
    public OrderId Id { get; }
    public OrderStatus Status { get; }
    public Money Total { get; }
    public List<OrderLineDto> Lines { get; }
---

Separate Models

Commands and queries use separate models. The write model is optimized for consistency and business rules. The read model is optimized for query performance and shape.

// Write model — enforces business invariants
public class Order {
    private OrderId id;
    private OrderStatus status;
    private List<OrderLine> lines;

    public void AddProduct(Product product, int quantity) {
        if (status == OrderStatus.Shipped)
            throw new CannotModifyShippedOrderException(id);
        lines.add(new OrderLine(product, quantity));
    }
---

// Read model — denormalized for fast queries
public class OrderSummary {
    public OrderId Id { get; }
    public string CustomerName { get; }
    public decimal Total { get; }
    public int ItemCount { get; }
    public string Status { get; }
---

Event Sourcing Fundamentals

Instead of storing the current state, event sourcing stores a sequence of events — every change to the application state is captured as an event.

Events as Source of Truth

Order Created (id=123, customer=456)
Product Added (order=123, product="Widget", qty=2)
Product Added (order=123, product="Gadget", qty=1)
Order Shipped (order=123, tracking="TRK789")

The current state is derived by replaying all events. This is the event stream — the authoritative record of what happened.

Rebuilding State from Events

class Order:
    def __init__(self):
        self.id = None
        self.items = {}
        self.status = "pending"

    def apply(self, event):
        if isinstance(event, OrderCreated):
            self.id = event.order_id
        elif isinstance(event, ProductAdded):
            self.items[event.product_id] = self.items.get(event.product_id, 0) + event.quantity
        elif isinstance(event, OrderShipped):
            self.status = "shipped"

    @staticmethod
    def from_events(events):
        order = Order()
        for event in events:
            order.apply(event)
        return order

Event Store

An event store is a database optimized for append-only event streams:

class EventStore:
    def __init__(self):
        self.streams = {}  # stream_id -> [events]

    def append(self, stream_id, events, expected_version):
        if stream_id not in self.streams:
            self.streams[stream_id] = []
        stream = self.streams[stream_id]
        if len(stream) != expected_version:
            raise ConcurrencyException()
        stream.extend(events)

    def read_stream(self, stream_id):
        return self.streams.get(stream_id, [])

Combining CQRS with Event Sourcing

CQRS and event sourcing complement each other naturally:

Command → Aggregate → Events → Event Store → Event Bus → Projections → Read Models
  1. A command is sent to the command handler
  2. The aggregate loads its events and rebuilds state
  3. The aggregate processes the command and produces new events
  4. Events are appended to the event store
  5. Events are published to an event bus
  6. Projections consume events and update read models
  7. Queries read from the optimized read models

Command Handler

class PlaceOrderHandler:
    def __init__(self, event_store, event_bus):
        self.event_store = event_store
        self.event_bus = event_bus

    def handle(self, command):
        events = self.event_store.read_stream(f"order-{command.order_id}")
        order = Order.from_events(events)

        new_events = order.place_order(command.customer_id, command.items)
        self.event_store.append(f"order-{command.order_id}", new_events, len(events))

        for event in new_events:
            self.event_bus.publish(event)

Read Model Projection

class OrderSummaryProjection:
    def __init__(self, db):
        self.db = db

    def handle(self, event):
        if isinstance(event, OrderCreated):
            self.db.execute(
                "INSERT INTO order_summaries (id, customer_id, total, status, item_count) "
                "VALUES (%s, %s, %s, %s, %s)",
                (event.order_id, event.customer_id, 0, "pending", 0)
            )
        elif isinstance(event, ProductAdded):
            self.db.execute(
                "UPDATE order_summaries SET total = total + %s, item_count = item_count + %s "
                "WHERE id = %s",
                (event.price, event.quantity, event.order_id)
            )

Consistency Models

CQRS with event sourcing is eventually consistent by nature. When a command executes, the write model updates immediately, but read models update asynchronously.

Eventual consistency means a user who places an order might not see it in their order list for a few milliseconds. For most applications, this is acceptable. For cases where it isn’t, use:

  • Immediate consistency: Process the read model update synchronously in the same transaction (sacrifices scalability)
  • Optimistic UI: Update the UI optimistically before the read model confirms
  • Temporal queries: Query the event store directly for the latest state

Benefits

  • Complete audit trail: Every state change is recorded forever. You can reconstruct any past state.
  • Temporal queries: What did the system look like at any point in time?
  • Scalable reads: Read models can be denormalized, cached, and scaled independently.
  • Event-driven: Other services react to events, enabling loose coupling.
  • Debugging: Replay events to reproduce bugs from production.

Trade-offs

  • Complexity: Event sourcing adds significant infrastructure complexity over a simple database.
  • Event schema evolution: Events are immutable. Handling schema changes requires careful versioning.
  • Learning curve: Teams need to understand event-driven thinking and eventual consistency.
  • Storage: Event stores grow unboundedly. Implement snapshotting and retention policies.
  • Querying: The event store does not support arbitrary queries efficiently. You must maintain projections.

When to Use These Patterns

Use CQRS and event sourcing when:

  • You need a complete audit trail (finance, compliance, healthcare)
  • You need temporal queries (undo, replay, what-if analysis)
  • Read and write workloads have different scale requirements
  • Your domain has complex business rules that benefit from event-driven workflows

Avoid when:

  • The domain is simple CRUD
  • Your team is small and unfamiliar with the patterns
  • Strong consistency is required for most operations
  • Storage costs or data volume are critical constraints

Getting Started

Start with CQRS without event sourcing — just separate your read and write models. Add event sourcing only if you need the audit trail or temporal query capabilities. Use a mature event store (EventStoreDB, Axon Server) rather than building your own. Implement snapshotting early to control replay costs. Invest in monitoring — eventually consistent systems need observability to detect when projections fall behind. CQRS and event sourcing are powerful tools, but they are tools of choice, not default. Use them where they solve real problems.

Event Store Implementation

An event store is a database optimized for append-only event streams. It stores events as an ordered sequence indexed by aggregate ID and version number. PostgreSQL, MySQL, and specialized stores like EventStoreDB each offer different tradeoffs. Key requirements: atomic append of events, optimistic concurrency control (checking aggregate version), and efficient replay of events for a specific aggregate. Projections subscribe to the event stream and build denormalized read models for query performance.

Snapshot Strategy

Replaying the entire event stream for an aggregate with thousands of events becomes expensive. Snapshots save the aggregate state at a specific version. On replay, load the latest snapshot and replay only events after the snapshot version. Configure snapshot frequency based on aggregate change rate — every 100 events is a reasonable default for most systems. Snapshots create a tradeoff between rebuild speed and storage efficiency.

Architecture Decision Records

Architecture Decision Records (ADRs) document significant architectural decisions and their rationale. Each ADR captures: the context (why the decision was needed), the decision (what was chosen), the alternatives considered (what was rejected and why), and the consequences (tradeoffs and implications). ADRs are stored in version control alongside the code, creating a historical record of design evolution. Benefits include: onboarding new team members faster, preventing repeated debates about past decisions, and providing rationale for future architects questioning why things are the way they are. Start an ADR repository with a lightweight template. Write ADRs for significant decisions that affect system architecture, technology choices, or design patterns. Review ADRs as a team before implementation.

Conway’s Law in Practice

Conway’s Law states that organizations design systems that mirror their communication structures. Teams that communicate frequently will produce tightly coupled components. Teams that communicate infrequently will produce loosely coupled components that communicate through well-defined interfaces. Use this law deliberately: align team boundaries with service boundaries (inverse Conway maneuver). If you want microservices, structure your teams around services. If you want a monolith, have one team. The organization chart and the architecture diagram should be congruent.

FAQ

What is the first step to learn this? Start by understanding the core principles and practicing with small, focused exercises before tackling complex projects.

Can I use this in production? Yes, these techniques are battle-tested in production environments. Always test thoroughly in your specific context.

Where can I find more resources? Official documentation, community forums, and the excellentwiki.com related articles provide comprehensive learning paths.

For a comprehensive overview, read our article on Api Design Principles.

For a comprehensive overview, read our article on Clean Code Guide.

Section: Software Design 1475 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top