CQRS and Event Sourcing in Production: When to Use Them and How to Get Them Right

Diagram showing command and query separation in a distributed system architecture

CQRS and Event Sourcing are frequently over-applied and under-understood. Used in the right context, they enable scalable read models, perfect audit trails, and resilient event-driven systems. Used unnecessarily, they add enormous complexity without proportional benefit.

Introduction

Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) are architectural patterns that appear frequently in discussions of microservices, DDD, and high-scale systems. They are powerful tools with specific, well-defined use cases. They are also two of the most frequently misapplied patterns in the industry, introduced into codebases that would have been simpler and more maintainable with standard CRUD approaches.

This guide explains both patterns deeply: what problems they solve, how they work in production systems, what the implementation looks like in Java/Spring Boot, and — critically — when you should not use them.

What is CQRS?

CQRS separates the model for modifying state (commands) from the model for reading state (queries). In a standard CRUD application, the same data model and often the same database tables serve both reads and writes. This simplicity is fine for most applications. But consider a high-traffic e-commerce order management system: the write model handles complex business logic — order creation, payment processing, inventory reservation — with strong consistency requirements. The read model must serve dashboards, customer order history, search indexes, and analytics with low latency across millions of records.

These two requirements conflict. Optimizing the write model for transactional integrity often creates slow, complex queries on the read side. Denormalizing for fast reads makes writes complex and brittle. CQRS resolves the tension by explicitly splitting them: commands go to a write stack optimized for correctness; queries go to a separate read stack optimized for performance and shape. Each stack can use different databases — PostgreSQL for writes, Elasticsearch or Redis for reads — and scale independently.

What is Event Sourcing?

Event Sourcing stores the history of state changes as an immutable sequence of events rather than storing only the current state. Instead of updating an orders table with the current order status, you append events like OrderPlaced, OrderConfirmed, PaymentProcessed, and OrderShipped to an event log. The current state of any entity is derived by replaying its events from the beginning (or from a snapshot).

This gives you several powerful properties: a complete, immutable audit log of every state change; the ability to rebuild any past state of the system; temporal queries (what was the state of this order at 14:32 yesterday?); and event replay for building new read projections from existing history without touching the source data.

CQRS + Event Sourcing: The Combined Pattern

CQRS and Event Sourcing are independent patterns — you can use either without the other — but they compose naturally. In the combined approach, command handlers process commands, apply domain logic, and emit domain events. These events are stored in an event store (the write side). Read models (called projections or view models) subscribe to these events and build denormalized, query-optimized representations. The read model never processes commands; the write model never fields read queries.

// Command Handler (Write Side)
@CommandHandler
public void handle(PlaceOrderCommand command) {
    validateInventory(command);
    validatePayment(command);
    AggregateLifecycle.apply(new OrderPlacedEvent(
        command.getOrderId(),
        command.getItems(),
        command.getCustomerId(),
        Instant.now()
    ));
}

// Event Sourcing Handler (Applies event to aggregate state)
@EventSourcingHandler
public void on(OrderPlacedEvent event) {
    this.orderId = event.getOrderId();
    this.status = OrderStatus.PLACED;
    this.items = event.getItems();
}

// Projection (Read Side)
@EventHandler
public void on(OrderPlacedEvent event, @SequenceNumber long seq) {
    OrderSummary summary = new OrderSummary(
        event.getOrderId(),
        event.getCustomerId(),
        "PLACED",
        event.getItems().size()
    );
    orderSummaryRepository.save(summary);
}

The above example uses Axon Framework's conventions. The aggregate processes commands through @CommandHandler, applies events through @EventSourcingHandler to update in-memory state, and an independent projection listener rebuilds the read model.

The Event Store: Design Requirements

The event store is the heart of an event-sourced system. It must provide append-only writes, ordered reads by aggregate ID and sequence number, global event streaming for projections, and optimistic concurrency control (rejecting writes if the expected version does not match). EventStoreDB is purpose-built for this. PostgreSQL can work with a well-designed events table. Kafka is not an event store by default (retention policies and compaction semantics differ from what you need for aggregate reconstitution), but can serve as an event bus for projections.

Projections: Building Read Models from Events

Projections consume the event stream and build denormalized read models optimized for specific query patterns. One projection might build an order summary table for the customer portal. Another might build an Elasticsearch index for operational search. A third might aggregate daily revenue figures for a business dashboard. Because projections are built from the event log, you can add new projections at any time and replay history to populate them without touching write-side logic.

Projection rebuilds should be tested regularly. Track the position (sequence number) of each projection's event consumption. If a projection falls behind due to downtime, it must catch up without affecting the write side or other projections.

Snapshots: Managing Long Aggregate Histories

If an aggregate accumulates thousands of events over its lifetime (a customer account, a long-lived order workflow), replaying all events on every command becomes slow. Snapshots capture the aggregate state at a specific sequence number. On reconstitution, the system loads the latest snapshot and then replays only events after that snapshot. Snapshot frequency is tunable: create one every 50, 100, or 1000 events depending on aggregate complexity and query patterns.

Performance and Scaling Considerations

Read models can be scaled independently of write models. If your product dashboard needs sub-100ms query times at 10,000 requests per second, you can scale the read store horizontally — Redis clusters, replicated PostgreSQL read replicas, or Elasticsearch — without touching the write-side event store. Write throughput scales through aggregate-level partitioning: events for different aggregates are independent and can be processed in parallel.

Eventual consistency between write and read models is the primary tradeoff. After a command succeeds, there is a brief lag before projections reflect the change. For most business flows, this is acceptable. For user-facing actions where you show immediate feedback (show the user their updated order status right after placing it), you can use the command response to update the UI optimistically while the projection catches up.

Pros and Cons

Pros: Perfect audit trail with no data loss. Ability to rebuild any read model from history. Temporal queries. Decoupled read and write scaling. Natural fit for event-driven architectures and microservices integration.

Cons: Significant architectural complexity compared to CRUD. Eventual consistency requires careful UX design. Event schema evolution (changing event structures) requires migration strategies. Debugging requires reconstructing event timelines. Not all teams have the operational maturity to manage event stores and projection lag.

Common Mistakes

Applying CQRS everywhere: CQRS adds meaningful complexity. Use it for domains where read and write scaling requirements differ significantly, or where audit and temporal query capabilities justify the investment. A simple blog or product catalog does not need CQRS.

Using Kafka as an event store: Kafka is excellent for event streaming, but topic retention and compaction policies make it a poor event store for aggregate reconstitution. Use EventStoreDB or a well-designed PostgreSQL events table for the authoritative event log.

Ignoring event versioning from day one: Events are immutable. When your domain model evolves, old events may need upcasting. Design event versioning strategy before your first production deployment, not after you have millions of events to migrate.

Skipping snapshots for long-lived aggregates: An aggregate with 10,000 events that reconstitutes on every command creates serious latency issues. Implement snapshotting before this becomes a production problem.

When NOT to Use CQRS and Event Sourcing

Do not use these patterns for simple CRUD-heavy applications where read and write models are nearly identical. Do not use them in small teams without prior experience in event-driven systems — the learning curve is steep and the debugging complexity is real. Do not use them when the business requires strong consistency between write and read views with no tolerance for lag. Simple applications should stay simple.

Key Takeaways

  • CQRS separates write models (optimized for correctness) from read models (optimized for query performance).
  • Event Sourcing stores the history of state changes as immutable events, enabling audit, temporal queries, and projection rebuilds.
  • They compose naturally but are independent — use either or both depending on actual requirements.
  • Use EventStoreDB or PostgreSQL for the event store; Kafka for event streaming to projections.
  • Design event versioning and snapshot strategies before your first production deployment.

Conclusion

CQRS and Event Sourcing are powerful architectural patterns with clear, well-defined use cases: high-scale systems where read and write requirements diverge significantly, domains that require complete audit history, and systems that benefit from event-driven integration with other services. They are not silver bullets. Apply them where they solve genuine problems, invest in the operational tooling to manage projections and event stores, and build team expertise before deploying to production. When used correctly, they enable systems that are auditable, scalable, and remarkably resilient to changing requirements.

Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Kubernetes · AWS · System Design

Portfolio · LinkedIn · GitHub

Related Articles

Share your thoughts

Have you used CQRS or Event Sourcing in production? Share your experience below.

← Back to Blog