Skip to content
Back to all concepts
ArchitecturePatternsData

Event Sourcing & CQRS

Comprehensive guide to Event Sourcing and Command Query Responsibility Segregation (CQRS) patterns, covering immutable event logs, state reconstruction, read/write separation, and real-world implementations in banking, e-commerce, and audit systems.

By ScaleWikiUpdated 4 min readAdvanced

History as the source of truth

Event sourcing stores accepted state changes as an ordered history. For an account, Opened, Deposited, and Withdrawn events can reconstruct the balance. CQRS separates command handling from query models. The two patterns can be used together, but CQRS does not require event sourcing or separate databases.

A CRUD database can also maintain an audit table. Choose event sourcing when replay, multiple projections, or domain history justifies the long-term cost of preserving and evolving events.

Trace one command through the system

Consider an illustrative inventory record with 10 available units at stream version 7. Two requests each try to reserve 8 units.

  1. Each handler loads version 7 and checks availability.
  2. Each proposes a Reserved(8) event with expected stream version 7.
  3. The event store atomically allows only one append at that version.
  4. The losing handler reloads version 8, sees 2 available units, and rejects or revises its request.

A read-then-insert check in application memory cannot enforce this across concurrent handlers. The store must enforce the version condition atomically, for example through a transaction and a uniqueness constraint on (aggregate_id, stream_version). A batch of events from one accepted command must also append atomically.

text
handle command with unique command_id
  if command_id already committed: return its saved result
  load aggregate and stream_version
  validate command against current state
  derive events
  atomically append events only if expected version still matches
  atomically record command result and publication intent
  return committed stream version

This is a workflow sketch, not a complete database implementation. Conflicts require a bounded retry policy and another validation pass. Use integer minor units or an appropriate decimal type for money, not floating-point arithmetic without an explicit rounding policy.

Reconstruct state with a deterministic reducer

python
def apply_inventory(available, event):
    kind, quantity = event
    if quantity < 0:
        raise ValueError("Negative quantities are not valid events")
    if kind == "StockAdded":
        return available + quantity
    if kind == "Reserved":
        if quantity > available:
            raise ValueError("Invalid history: reservation exceeds stock")
        return available - quantity
    raise ValueError("Unknown event type")

available = 0
for event in [("StockAdded", 10), ("Reserved", 8)]:
    available = apply_inventory(available, event)
assert available == 2

The reducer neither charges a card nor sends an email. Replaying history must not repeat external side effects. Those belong to separately tracked workflows with idempotency and reconciliation.

Publish events without a dual-write gap

Saving an event and then sending it to a broker can fail between the two operations. Persist an outbox record in the same transaction as the event, then publish asynchronously. If the worker crashes after delivery but before marking the outbox row complete, the message may be delivered again.

Consumers therefore need event identifiers and an idempotent update. Store the deduplication marker and projection change in the same transaction. Merely checking an in-memory set before incrementing a counter is not sufficient after a restart. The transactional outbox pattern describes this boundary.

Design the read model for its query

A product-search projection can optimize keywords and facets while an order-history projection stores exactly the fields needed by that view. Asynchronous projections can lag behind accepted commands.

Return a committed version to the caller. A subsequent read can wait for at least that version, with a timeout and an honest “processing” state. An immediate second write to a different read store does not create atomicity; it creates another failure boundary.

A rebuild should write to a fresh projection, replay to a checkpoint, catch up with the live stream, validate, and switch readers. Replaying increments into the existing projection would double-count data unless the handler deduplicates events.

Snapshots and event evolution

A snapshot contains state plus the stream version it represents. Load it, then replay later events in stream order. Choose snapshot frequency from measured replay time and storage cost, not a universal “every 100 events” rule.

Keep stream version separate from event schema version. An upcaster can translate an old representation while preserving its historical meaning. It must not turn stream event 500 into version 2 or invent a currency that the historical record never established.

Event history is not automatically a perfect audit record. Protect integrity, restrict access, capture relevant actor/context information, and define how sensitive data is retained or removed. Immutability of the logical event model does not remove those responsibilities.

A recovery exercise

Kill the publisher after broker delivery but before its acknowledgement is recorded. Restart it and verify that the projection still counts the event once. Next rebuild the projection from a snapshot plus later events and compare it with a complete replay. Finally send concurrent reservations against the last few units and verify that the atomic version check preserves the invariant.

Microsoft's event sourcing pattern and CQRS pattern describe the architectural trade-offs. See also distributed transactions and Kafka architecture.