The current balance cannot explain its own history
A hypothetical account database says the balance is 300. A customer asks which sequence of decisions produced it. Separately, the analytics team wants a query-shaped view that would make transactional writes awkward.
These are two problems. They do not require one inseparable architecture.
Choose the source of truth
Event sourcing stores durable domain events as the authoritative history and derives state by replay. CQRS separates command and query models; it can also be used with ordinary current-state storage plus a change stream. An audit log is not automatically an event-sourced model, and an event bus is not automatically the authoritative event store.
If the only requirement is a fast search view of a mutable catalog, must you rewrite the domain as events?
An append still has a concurrency decision
Suppose two commands load aggregate version 4 and each proposes version 5. Enforce an atomic expected-version append or uniqueness/transaction protocol for the aggregate's next version. One succeeds; the other reloads and reevaluates its business rule. It must not blindly relabel its old decision as version 6.
pseudocodestate = hydrate(snapshot, subsequent events) check command identity and expected version new_events = decide(state, command) atomically append against the expected version return committed version
A snapshot at version 5,000 plus events 5,001–5,023 rebuilds version 5,023. It is a cache of the history, with a schema and checksum. Snapshot frequency can reduce replay work, but background snapshot lag means a nominal interval is not necessarily a hard upper bound on deltas.
Replay must not repeat the world
A projection consuming FundsDeposited cannot simply add the amount every delivery. Store its applied version or event identity atomically with the projection update. For delta events, apply the next expected version and park gaps; skipping straight to version 8 can lose version 7's contribution. Full-state replacement events may permit different gap rules.
Rebuild a projection into a new generation, catch up to a known source position, verify it, then atomically switch readers. Keep the old generation until rollback is safe. Do not replay email or payment side effects just because you are rebuilding a search index.
The command response can return the committed aggregate version. A client needing read-your-writes waits until the projection reaches that version or uses the authoritative command result. Reporting successful command acceptance as if every projection were immediately current creates the old replica-read puzzle again.
Old events meet new code
Prefer compatible schema evolution and versioned event types. Upcasters can translate old representations in memory, but cannot invent information that the old event never captured. Record facts needed for deterministic historical meaning—such as the chosen price—rather than consulting today's external state during replay.
Append-only history is not inherently tamper-evident or a complete compliance solution. Access controls, independent audit evidence, backup/restore, retention, and personal-data handling still need explicit design. Avoid storing unnecessary sensitive data in permanent events.
Measure projection lag, gap count, duplicate rate, replay throughput, and command conflict retries. Ordinary CRUD with constraints and an audit trail may be easier to operate when full event replay brings little value.
Mental model: event sourcing chooses history as truth; CQRS chooses different shapes for decisions and questions. Use either when its particular benefit is worth its recovery obligations.
Transfer: the tax calculation
A historical order event stores only product ID and quantity. Replaying it looks up today's tax rate. Will replay reproduce the original charge?
Source trail: 23-event-sourcing-and-cqrs-architecture. See the source map for consolidation and reference-retained material.