Event Sourcing and CQRS: Designing Auditable, Evolvable Systems

Event Sourcing and CQRS: Designing Auditable, Evolvable Systems

Event Sourcing and CQRS: Designing Auditable, Evolvable Systems

Most applications store only the current state of their domain. A customer row shows the latest address, an order row shows the latest status, and a balance row shows the latest amount. That approach is simple and works well for many systems. But when the business needs to understand how it arrived at the current state, reconcile disputes, or build new views of the same data, a state-only model can become limiting. Event sourcing offers a different foundation: persist the sequence of domain events that caused state changes, then derive current state by replaying those events.

Command Query Responsibility Segregation, or CQRS, is a related but distinct pattern. It separates the model that handles writes from the model that serves reads. Event sourcing and CQRS are often used together because events provide a natural stream for updating read models. They can also be adopted independently. This article explains both patterns, how they fit together, and the trade-offs teams should weigh before committing to them.

What Is Event Sourcing?

Event sourcing treats every state change as an immutable event. Instead of updating a row in place, the application appends an event to an ordered log. The current state is a projection built from those events. For example, a bank account might store events such as AccountOpened, MoneyDeposited, MoneyWithdrawn, and FeeCharged. The balance is calculated by applying those events in order. The event log becomes the source of truth.

Events are facts about the past. They are named in the past tense, such as OrderSubmitted or PaymentAuthorized. They should describe something meaningful to the business, not merely a database update. An event store is typically append-only. Existing events are not overwritten or deleted in normal operation. If a correction is needed, a new compensating event is appended.

What Is CQRS?

CQRS splits an application into a command side and a query side. The command side handles requests that change state. It validates business rules and writes data. The query side handles requests that read data. It can use a different schema, database, or data store optimized for the specific read patterns.

The separation does not require two databases or a distributed system. It can be as simple as separate command and query classes in the same process. The core idea is to avoid forcing one model to serve every purpose. Write models tend to protect invariants and enforce consistency. Read models tend to be denormalized, fast, and shaped for user interfaces or reports.

How Event Sourcing and CQRS Work Together

When combined, the write side validates commands and appends events to the event store. Projectors or event handlers subscribe to those events and update one or more read models. Queries are served from the read models, not from the event store. This flow allows multiple read models to be built from the same event stream. One projection might support a customer order history. Another might feed a fulfillment dashboard. A third might power analytics.

The write side remains focused on business invariants. The read side remains focused on query performance. The event stream becomes the integration point between them. This decoupling can make the system easier to evolve, but it also introduces eventual consistency between writes and reads. Teams must design for that consistency model rather than pretend it does not exist.

Practical Example: Order Management

Consider an order management system. On the command side, a customer submits an order. The command handler loads the current order state by replaying its events, checks whether the order can be submitted, and appends an OrderSubmitted event. Other events might include OrderCreated, ItemAdded, ItemRemoved, PaymentAuthorized, OrderShipped, and OrderCancelled.

On the query side, projectors listen for those events and maintain read models. A current order summary projection stores the order status and total. A customer history projection stores past orders for a customer profile. A fulfillment queue projection stores orders that are ready to ship. Each projection can be rebuilt by replaying the event stream from the beginning, or from a snapshot plus later events.

A simplified command handler might look like this:

class SubmitOrderHandler {
  handle(command) {
    const order = orderRepository.load(command.orderId);
    order.submit(command.customerId);
    orderRepository.save(order);
  }
}

The repository loads events, applies them to rebuild the order aggregate, and appends new events when the aggregate changes. The aggregate enforces rules such as not submitting an empty order or not submitting an order that is already cancelled. The read side never has to reconstruct those rules for every query.

Benefits of Event Sourcing

  • Audit trail: The event log provides a durable history of what happened and in what order.
  • Temporal queries: It becomes possible to ask what the state was at a given point in time by replaying events up to that point.
  • Multiple read models: New projections can be built from existing events without changing the write model.
  • Debugging and reconciliation: Incidents can be investigated by examining the sequence of events rather than only the final state.
  • Decoupled integration: Other systems can subscribe to events and react without the write model knowing about them.
  • Evolvability: New business views can be added by projecting old events in new ways.

Trade-offs and Challenges

Event sourcing is not a default architecture. It introduces complexity that many CRUD applications do not need. The event schema becomes a long-lived contract. Changing an event after it has been stored requires careful versioning and upcasting. Projections can lag behind the write side, creating eventual consistency. Users may write a command and then query a read model that has not yet been updated.

Storage also requires planning. The event log grows continuously. Snapshots can reduce replay time, but they add another moving part. Projectors need to be idempotent because events may be delivered more than once in some systems. Privacy requirements can be difficult because immutable events may contain personal data. Teams may need encryption, tokenization, or separate data stores for sensitive fields.

Operational maturity matters. The team must be comfortable with asynchronous processing, monitoring projection lag, handling failed projectors, and rebuilding read models. If the domain is simple and the organization is not prepared for these demands, a traditional state-based model may be the better choice.

Designing Events

Event design is a critical part of the approach. Poorly designed events create long-term coupling and make projections harder to build. Consider the following guidelines:

  • Name events in the past tense and use business language, such as InvoiceIssued rather than InvoiceRowUpdated.
  • Include a unique event identifier, a timestamp, and metadata such as the aggregate identifier and correlation identifier.
  • Keep events focused on a single business fact. Avoid events that describe multiple unrelated changes.
  • Avoid exposing internal database structures. Events are part of the domain model, not a dump of a table.
  • Plan for versioning from the beginning. Add new fields in a backward-compatible way when possible.
  • Be cautious with sensitive data. Store only what is necessary and protect it appropriately.

Snapshots and Replay

Replaying a long event stream for every command can become slow. A snapshot captures the state of an aggregate at a specific event version. To load an aggregate, the system reads the latest snapshot and then applies only the events that occurred after it. Snapshots are an optimization, not the source of truth. They can be deleted and rebuilt from the event log if needed.

Projections can also be rebuilt from scratch. This is useful when a bug is found in a projector or when a new read model is introduced. A rebuild may take time and consume resources, so teams often run it alongside the live system and switch over when the new projection has caught up.

Consistency and Transactions

On the write side, consistency is usually enforced per aggregate. A command appends one or more events atomically for that aggregate. Optimistic concurrency control can prevent two commands from modifying the same aggregate at the same time. If the expected version does not match, the command can be retried or rejected.

Across aggregates and between the write and read sides, consistency is often eventual. A command may succeed while a projection is still processing. The user interface can handle this by showing a pending state, by reading from the write model for the immediate response, or by waiting for the projection to catch up. There is no single correct answer. The trade-off is between simplicity, latency, and user experience.

When to Use Event Sourcing and CQRS

These patterns tend to fit domains where history, auditability, and complex business rules matter. Examples include financial ledgers, order management, inventory tracking, insurance claims, and collaborative workflows. They also fit systems that need to support multiple read views or integrate with many downstream consumers.

They tend to be a poor fit for simple CRUD applications, small internal tools, or teams that need immediate consistency everywhere with minimal operational overhead. CQRS alone can be useful without event sourcing when read and write workloads have very different scaling or modeling needs. Event sourcing alone can be useful without CQRS when the primary goal is an audit log or temporal state, though many teams combine them because the event stream naturally feeds read models.

Operational Considerations

Running an event-sourced system requires attention to a few areas. The event store must be durable, backed up, and monitored. Projection lag should be visible. Failed projectors should be retried or paused without corrupting read models. Access to the event log should be controlled because it contains the full history of the system. Retention and archival policies should be defined, especially when events contain personal or regulated data.

Testing also changes. Command handlers can be tested by arranging events and asserting new events. Projectors can be tested by feeding known event sequences and checking the resulting read model. Integration tests can verify that the write side and read side work together under expected delays and duplicate deliveries.

Conclusion

Event sourcing and CQRS are powerful patterns for systems where history is a first-class concern and different parts of the application need different views of the same data. Event sourcing stores facts as an append-only log. CQRS separates commands from queries. Together, they can improve auditability, enable temporal queries, and support multiple read models.

They are not free. They add eventual consistency, schema evolution challenges, projection operations, and storage growth. The right approach is to evaluate the domain, the team, and the operational constraints. Start with a bounded context where the benefits are clear. Model events carefully. Build one projection at a time. Measure the complexity against the value. When history and flexibility matter more than immediate consistency everywhere, these patterns can be a strong foundation.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *