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.
- Each handler loads version 7 and checks availability.
- Each proposes a
Reserved(8)event with expected stream version 7. - The event store atomically allows only one append at that version.
- 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.
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
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.
Keep reading
Caching Strategies
A breakdown of where to place your cache and how to keep it in sync with your database.
Microservices Architecture
An architectural style that structures an application as a collection of loosely coupled, independently deployable services.
Scaling: Overview
A practical guide to scaling up and out, testing database and cache bottlenecks, and estimating failure headroom from measured workload rather than user counts.
API Gateway Pattern
The single entry point for microservices. Implementing rate limiting, authentication, and protocol translation/aggregation.
Backpressure: Bound Work Before It Becomes a Backlog
Calculate queue growth, propagate bounded demand, and choose an explicit overflow policy using a runnable asynchronous queue example.
Bloom Filters: Size, False Positives and Safe Use
Calculate the memory budget, run a bit-packed example, and understand when a probabilistic membership check must defer to an authoritative store.