Skip to content

VOL 06 / CH 15 / LESSON 03

15.3 CQRS and Event Sourcing: Separate Models and Store Facts ​

An order system needs both fast queries of the current state and the ability to explain how that state evolved over time. To address this, the team proposes splitting the read and write models while preserving the event history.

CQRS and Event Sourcing often appear together, but they solve different problems: CQRS separates the write model from the read model; Event Sourcing treats the sequence of domain events as the authoritative record of state. You can use either one, or neither.

CQRS: Use Separate Models for Reads and Writes ​

The write model enforces invariants and is typically structured around aggregates and commands. The read model, in contrast, pre-aggregates data for specific queries, organized by pages, reports, or API responses.

text
Command → Write Model → Database
                         │
                         └─ events / CDC → Projection → Read Store → Query

CQRS does not require two separate databases. The lightest implementation can use different object models or views within the same database. Storage separation is only necessary when there are distinct differences in load, permissions, deployment, or storage characteristics.

The diagram uses an asynchronous projection, so read lag needs an explicit contract. Maintaining a read model synchronously in the same transaction avoids this asynchronous wait. With asynchronous updates, a successful command does not guarantee a current projection. Clients may need to:

  • Accept temporarily stale data and display a timestamp indicating when it was last updated;
  • Retrieve key results directly from the write model;
  • Use version tokens to wait for the projection to catch up;
  • Provide a dedicated path for "read-your-own-writes" scenarios.

Event Sourcing: State Is Folded from Events ​

Traditional persistence stores the current state; Event Sourcing stores the domain facts that led to that state:

text
EntryRequested(v1)
SeatReserved(v2)
PaymentAuthorized(v3)
RegistrationConfirmed(v4)

This Java method is a project fragment using the domain types Registration and RegistrationEvent. empty() returns initial state. apply() must validate that events belong to one registration, have consecutive versions, and represent legal transitions, then return the resulting state. The method only folds its input in order; it neither sorts events nor queries storage:

java
Registration rehydrate(List<RegistrationEvent> events) {
    Registration state = Registration.empty();
    for (RegistrationEvent event : events) {
        state = state.apply(event);
    }
    return state;
}

Replay restores in-memory state. It must not charge again, send notifications again, or query today's exchange rate. Amounts, timestamps, rates, or rule versions needed for historical calculations must be recorded as facts; side effects belong in a separate processing path. Rebuilding a projection should not send every participant another registration text.

Appended events must include an expected version to prevent two commands from simultaneously writing to the same old state:

text
append(streamId, expectedVersion=4, newEvents)

The event store must atomically check the version and append the entire batch. Reading the version and then inserting unconditionally is insufficient. If the current version is no longer 4, the caller must reload and reconsider the business command, not blindly resend events derived from old state. A command ID can also reveal whether an append succeeded before its response was lost.

Events Are Long-Term Contracts ​

Once written into history, events may be replayed years later. Event evolution strategies include:

  • Upcasting older versions to the current in-memory representation when reading;
  • New consumers that understand multiple schema versions simultaneously;
  • Appending corrective events to fix facts without altering previously recorded history;
  • Retaining the data or stable version references needed for replay, without assuming mutable external objects will retain their old values.

Event names should express actual business facts, such as PaymentAuthorized, rather than vague or ambiguous references like RegistrationUpdated.

Snapshots and Projection Rebuilding ​

Snapshots are used to reduce the load time of aggregating long event streams, but they are not authoritative facts. Snapshots should record the event version they cover, enabling discard and reconstitution from events in case of corruption.

Discarding a snapshot is safe only while the original events remain complete and readable. If retention rules prune those events, the snapshot becomes required recovery data.

Projection processors need idempotency and checkpoints. Commit a projection update and its checkpoint in the same target transaction. If they cannot share a transaction, persist the projection first, then advance the checkpoint, and make replay idempotent. Advancing the position first can permanently skip data. To rebuild a large projection:

  1. Build from scratch in a new table or new index;
  2. Continuously catch up with newly arriving events;
  3. Validate counts and business invariants;
  4. Atomically switch read traffic;
  5. Maintain a rollback window.

Before switching, verify that the new projection has caught up to an explicit event position, not just a wall-clock time. If the old projection stops consuming during the rollback window, it becomes stale. Keep it updated or catch it up to the required position before switching back.

Don't Mistake Audit Logs for Event Sourcing ​

Recording "who changed which column when" has audit value, but it doesn't necessarily contain the complete semantic information needed to reconstruct business state. Event Sourcing requires events to be the authoritative source of state, ordered and verifiable, with a manageable evolution path, and supported by projection and recovery tooling.

Judging Whether to Adopt Event Sourcing ​

Signals indicating that Event Sourcing is worth considering:

  • The historical facts themselves hold core business value;
  • There is a need to trace a specific point-in-time state or recompute a new projection;
  • The domain naturally expresses itself through commands and events;
  • The team has the capability to manage versions, replay events, maintain snapshots, and perform recovery procedures.

Signals indicating that Event Sourcing is not suitable:

  • It's merely a standard CRUD operation;
  • The team expects it to “solve auditing automatically”;
  • Stable event semantics cannot be clearly defined;
  • The team lacks the ability to test event replay or disaster recovery.

References ​

Built with VitePress | Software Systems Atlas