Event Sourcing and CQRS: Adoption, Atomicity, and Replay Boundaries

Decide whether event history justifies event sourcing, then specify command retries, projection checkpoints, ordering, versioning, and side-effect-safe replay....

Decision brief

Event sourcing makes retained domain events the authoritative record from which specified state can be derived. That can be valuable when the meaning and sequence of changes matter. It also creates a long-lived obligation to preserve interpretable history, operate replay, and manage the effects of changing event contracts.

CQRS is a different choice: separating command and query models. It does not inherently require event sourcing, separate databases, asynchronous processing, or a message broker. Combining these choices should follow a concrete requirement, not a presumed architecture maturity ladder.

This paper is for backend architects, service owners, and data-platform engineers evaluating one bounded context. It argues for explicit transaction, ordering, and recovery contracts before selecting a store. The proposed stock-reservation scenario and code fixture are illustrative, not a customer implementation or a production readiness claim.

The scope excludes regulatory interpretation, product licensing comparisons, and guarantees of complete audit history or recovery. The document remains reviewed, non-indexable, and factually unapproved. Its executable model tests a small piece of logic; storage, concurrency, security, and disaster recovery still need implementation-specific evidence.

1. Separate the three architecture choices

| Choice | What it changes | What it does not establish | | --- | --- | --- | | CQRS | Different models or interfaces for commands and queries | Separate storage, asynchronous consistency, or event history | | Event sourcing | The authoritative representation of specified state is a retained event sequence | Complete business history, correct decisions, or automatic audit compliance | | Asynchronous projection | A read model follows committed changes through a separate processing path | A universal lag bound or immediate read-your-writes |

Microsoft's CQRS pattern guidance distinguishes simpler shared-store designs from more complex implementations and discusses the tradeoffs of combining CQRS with event sourcing. Use that distinction to avoid accepting several operational burdens for a requirement that needs only one.

For example, a complex reporting query may justify a separate read model without changing the write model to event sourcing. A requirement to record changes may be satisfied by an appropriately designed audit record or temporal data model. Evaluate those alternatives before making events the source of truth.

2. State the requirement that history must answer

Write the actual historical question. “We need auditability” is too broad to choose a persistence model. Does the business need to know which authorized actor requested a change, what the accepted decision was, or what a particular user saw at a past time? Those require different evidence.

A proposed adoption brief should include:

  • The historical or state-reconstruction question and its owner.
  • Which decisions and external inputs must be retained to answer it.
  • Required retention, access, correction, and deletion behavior.
  • Acceptable projection delay and command response semantics.
  • Expected growth, recovery objectives, and operator capacity.
  • A simpler alternative and the specific requirement it cannot satisfy.

Reject a proposal that relies only on anticipated flexibility. Future projections can use only information that was retained with adequate meaning and quality. They cannot reconstruct a business fact that the system never recorded.

Martin Fowler's foundational Event Sourcing discussion explains state reconstruction and the complications of external interactions. That model informs the design; it is not evidence that every application benefits from storing its state this way.

3. Define the illustrative domain and its invariant

Assume a service manages available units for a single stock item at a location. Commands request a reservation or release. Events record accepted changes. The write-side invariant is that an accepted reservation cannot make available units negative.

This is deliberately narrower than a complete ordering, payment, or warehouse system. A transfer between locations or a reservation across several items introduces additional transaction and workflow decisions. Do not infer those guarantees from a successful single-stream example.

For each command, record an authenticated scope, command identifier, payload identity, expected stream version, and requested operation. For each event, record a stable event identifier, stream identifier, stream version, event type, schema version, and the business data required by the chosen reducer.

Do not store arbitrary request bodies or personal details simply because future debugging might benefit. Event content is a retention and access commitment. Keep integration messages separately designed so internal history is not automatically exposed to every consumer.

4. Put the transaction boundaries in the design

The proposed command transaction commits the event append and its command receipt together. The separate projection transaction commits the read-model change and its checkpoint together. These are required properties of this design, not capabilities assumed of every store.

5. Resolve ambiguous command acknowledgements

A timeout after append does not prove that the command failed. The store may have committed while the response was lost. Retrying with a new identifier can apply the business operation twice.

The command receipt should be unique within its authenticated scope and tied to a canonical payload identity. A repeated command with the same identifier and matching payload can return the recorded result after authorization. Reuse with different content should be rejected, not interpreted as another attempt.

In the proposed transaction, recheck the expected version, enforce the invariant, append events, and save the receipt atomically. Concurrent attempts need a uniqueness or equivalent serialization mechanism. A preliminary receipt lookup alone does not prevent a race.

If the version conflicts, reload and re-evaluate the business decision under a bounded retry policy or return a conflict. Do not blindly append the events calculated from stale state. Specify receipt retention: once a receipt expires, deduplication may no longer protect a very late retry. The API contract must expose that limit.

6. Require only the ordering the invariant needs

For the stock stream, reservations and releases must be evaluated against the agreed per-stream sequence. A timestamp is not a reliable substitute for that sequence when clocks differ or events arrive late.

A cross-stream projection does not automatically require a global total order. A sum of independently deduplicated counts may be commutative across streams. In contrast, a projection that interprets one event as “the next action after another stream's decision” needs an explicit causal or coordination rule.

Test this distinction with a counterexample: merging independent counts in different orders should preserve the result, but evaluating a reservation before the stock-opening event should fail or wait. The ordering contract should say which case applies and what the processor does with a gap.

Cross-aggregate consistency is an implementation decision. A store may support transactions across multiple streams, or the design may coordinate through a durable workflow. Document the selected boundary and its availability, contention, and recovery costs. Do not call eventual consistency unavoidable merely because the design uses events.

7. Make projection progress atomic and inspectable

If a worker updates a read model and crashes before saving its checkpoint, the event may be processed again. If it advances the checkpoint before the update commits, the read model can permanently skip the event.

For the proposed design, update the projection and its processed-event/checkpoint state in one transaction. Acknowledge transport progress only after that transaction commits. If the checkpoint is in a different system, provide an equivalent protocol with a demonstrated failure model; do not assume two successful writes are atomic.

Partition checkpoints according to the actual ordering domain. A single integer is not necessarily a meaningful progress marker across independent partitions. Preserve enough provenance to identify the reducer version and source position represented by a model.

Unknown event types, incompatible schemas, and sequence gaps need an explicit stop or quarantine policy. Silently skipping them can make the projection appear current while its meaning is incomplete.

8. Use an executable model, then test the real store

The JavaScript fixture below models one projection transaction using copy-on-write state. It checks duplicate delivery, a simulated failure before commit, changed content under a reused event identifier, a sequence gap, and deterministic rebuild. It is not a database implementation and does not test concurrent writers, process termination, disk durability, or network failure.

import assert from "node:assert/strict";

const empty = () => ({ units: 0, version: 0, seen: {} });

function project(state, event, failBeforeCommit = false) {
  const fingerprint = JSON.stringify(event);
  const prior = state.seen[event.id];
  if (prior !== undefined) {
    if (prior !== fingerprint) throw new Error("event identity conflict");
    return state;
  }
  if (event.version !== state.version + 1) throw new Error("sequence gap");
  const next = {
    units: state.units + event.delta,
    version: event.version,
    seen: { ...state.seen, [event.id]: fingerprint }
  };
  if (failBeforeCommit) throw new Error("simulated failure");
  return next;
}

const opened = { id: "e1", version: 1, delta: 8 };
const reserved = { id: "e2", version: 2, delta: -3 };
let state = empty();

assert.throws(() => project(state, opened, true), /simulated failure/);
assert.deepEqual(state, empty());
state = project(state, opened);
assert.deepEqual(project(state, opened), state);
assert.throws(
  () => project(state, { ...opened, delta: 9 }),
  /event identity conflict/
);
assert.throws(() => project(empty(), reserved), /sequence gap/);
state = project(state, reserved);
const rebuilt = [opened, reserved].reduce((s, e) => project(s, e), empty());
assert.deepEqual(rebuilt, state);
assert.equal(state.units, 5);
assert.equal(state.version, 2);

The fixture deliberately omits authorization and write-side inventory validation. It also retains all processed identifiers in memory, which is not a production storage plan. A real implementation needs bounded storage and retention semantics consistent with its retry and replay guarantees.

Run the same logical cases against the selected store, including failure between every durable operation. Passing this model proves only that these example assertions hold.

9. Separate replay from external effects

A reducer used for state reconstruction should not send email, charge a payment method, or invoke a production integration while replaying historical events. An external effect cannot be undone merely by rebuilding a projection.

Use a separate effect-execution contract with durable intent, stable idempotency identity, recorded attempts, and reconciliation for ambiguous results. A provider's idempotency behavior, scope, and retention must be verified; a local “sent” flag does not prove an external action happened exactly once.

Give rebuild workers credentials and network permissions that exclude effect execution. A runtime flag is useful but should not be the only boundary. Test the rebuild with external-effect attempts blocked and recorded.

Reliable publication does not always require a separate outbox table. A suitable native durable subscription, change-data capture mechanism, or transactional outbox can bridge committed state to a consumer. Compare their acknowledgement boundary, retention, ordering, duplicate behavior, and recovery procedure for the selected product and version.

10. Version meaning, not just payload shape

An additive field can still change interpretation. A new currency, unit, timezone, or business-rule meaning can break a consumer even if its JSON parser accepts the payload. Record semantic compatibility as well as schema compatibility.

Keep representative historical fixtures and reducer versions. An upcaster must not invent a past fact that was never recorded. If an older event lacks information required by a new model, define an explicit unknown state, a justified enrichment source with provenance, or a limited reconstruction scope.

Distinguish reconstruction of the state the application originally produced from recalculation under corrected business rules. These are different questions and may yield different answers. Preserve the version and external-input assumptions that make each answer interpretable.

Snapshots can reduce loading work, but their validity depends on stream position and model version. Test rejection or replacement of an incompatible snapshot. If older events are removed and the snapshot becomes necessary for recovery, it is no longer merely a disposable optimization.

11. Bound rebuild and recovery claims

A rebuild needs complete relevant history, interpretable versions, available external inputs where required, and sufficient capacity. It cannot promise recovery after the authoritative data or necessary keys are lost.

Build a new projection beside the existing one. Capture the intended source boundary, process the retained history, catch up live changes, and reconcile domain-specific invariants before switching reads. Keep the old projection until the new one meets the acceptance criteria and the cutover recovery path is understood.

A projection switch does not reverse events, external actions, or a schema contract already consumed elsewhere. A compensating event records a new business action; it is not time travel. Compensation can fail or require human approval, so durable workflow state should expose unresolved cases.

Restore the event store and projection infrastructure in an isolated exercise. Measure recovered positions, missing intervals, key access, and the time required to rebuild usable queries. Replication and backup configuration alone do not establish the recovery outcome.

12. Design retention and access before recording forever

“Append-only” describes an intended write behavior or storage capability. It is not a universal legal requirement to retain every event permanently, nor proof that administrators cannot alter storage.

Have qualified privacy and security owners determine the applicable retention, correction, legal-hold, and deletion requirements. Minimize personal data in events and evaluate separated references, protected payloads, or other justified designs. Encryption alone does not establish that an erasure request has been satisfied across backups and derived stores.

Document event-store access separately from projection access. Administrative operations, replay jobs, exports, and integration consumers can expose history beyond what an ordinary application query permits. Apply tenant and purpose boundaries throughout.

If retention removes information needed for replay, state the resulting reconstruction limit. A narrower honest capability is preferable to a promise that every historical state will always be recoverable.

13. Build a failure-test and adoption packet

| Test | Acceptance evidence to retain | | --- | --- | | Response lost after command commit | Same command returns its recorded result without another accepted effect | | Two commands race on one stream | Version and uniqueness controls preserve the invariant | | Worker crashes around projection commit | Neither a skipped update nor duplicate application after recovery | | Event gap, duplicate, or schema conflict | Explicit handling and visible blocked progress | | Rebuild runs against historical events | Same defined model result, no external effects, documented exclusions | | Projection falls behind | Query behavior exposes freshness or waits only within a bounded policy | | Store or key material becomes unavailable | Rehearsed recovery with measured loss and access limits | | Retention removes required history | Reconstruction fails explicitly or uses the approved alternative |

Attach the domain brief, competing persistence option, store/version capability checks, command contract, projection protocol, event fixtures, access design, retention review, and operator runbooks. Include failed tests and unresolved risks.

A go decision requires a benefit that the simpler alternative cannot adequately provide and an owner able to maintain the event contracts and recovery procedures. Otherwise, defer adoption or use CQRS without event sourcing.

14. Choose a store by its failure contract

Do not choose the event store from a feature checklist alone. Ask the storage owner to demonstrate a specific append contract: two writers read stream version seven, each proposes version eight, and only one proposal can commit under the chosen concurrency rule. The other must observe a conflict and re-evaluate its command. A uniqueness constraint on stream and version may help implement that rule in a relational design, but it is not a complete application protocol. The transaction must also protect the receipt and the invariant being evaluated.

If the implementation uses PostgreSQL, distinguish the configured isolation level from the business guarantee. The PostgreSQL 17 transaction-isolation documentation describes the behavior and retry obligations of its isolation levels. An application using serializable transactions must be prepared to retry a transaction that is rejected for serialization reasons. Retrying only the final insert can reuse a decision made from invalid assumptions. Conversely, selecting a stronger isolation label does not fix an external side effect performed before commit.

Build a capability worksheet for the actual product and version. Record append limits, concurrency checks, acknowledgement durability, transaction scope, subscription progress, retention controls, backup recovery, and supported administrative operations. Run the conflict exercise with process termination and response loss, not only orderly exceptions. Inspect the resulting events and receipts after reconnecting. Ask what survives a node failure, a regional failure, and an operator mistake separately; they do not share one durability assumption.

Native subscriptions, change-data capture, and an outbox each introduce different responsibilities. A native subscription must preserve enough history and progress information for a stopped consumer. CDC must handle connector offsets, schema changes, and retained source logs. An outbox must be committed with the authoritative change, then drained and retained according to a recoverable protocol. AWS's transactional outbox guidance explains the dual-write problem and duplicate-processing concern. Debezium's outbox event router documents one concrete mapping from an outbox table to emitted events. Neither reference establishes the behavior of an untested installation.

Separate event storage from distribution. A broker may be an excellent delivery mechanism while lacking the command-side stream lookup or concurrency contract the application needs. Likewise, adding a second event store solely for a familiar product name creates another replication and recovery boundary. Record which system is authoritative and exactly when a command is accepted. No consumer should need to infer that answer from whichever system is currently reachable.

15. Give read-after-write behavior a product contract

After reserving three units, a user should not have to guess whether an unchanged availability screen means failure. One option is for the command response to return its accepted result and stream version. The client can display that result as pending projection visibility. Another is to query the authoritative state for this narrow confirmation. A third is a bounded wait until the required projection position is observed. Each option has a different latency, load, and implementation cost.

For a single-stream read model, an accepted stream version can be a useful minimum-freshness token. For a projection combining several streams or partitions, a single version from one stream is not a global freshness proof. Return or retain a position structure that corresponds to the actual ordering domains, and make the query service compare like with like. A wall-clock timestamp alone can be misleading when processing pauses, clocks differ, or a failed partition stops advancing.

Choose behavior for an expired wait before it happens. The API might return a pending state with a status resource, a documented stale result, or an explicit unavailable response. It must not silently report that the reservation was rejected when the command was already accepted. Give clients a bounded polling policy and cancellation behavior. An immediate refresh loop across many clients can add pressure to the service that is already behind.

For the stock example, the command invariant belongs on the write side. A delayed read model can help users browse availability, but it cannot authorize another reservation by itself. Revalidate available units inside the protected command decision. This is especially important when a pleasant optimistic UI makes two concurrent users believe the same units remain available. The product can acknowledge that delay without weakening the acceptance rule.

Review these choices with the workflow owner, not only the database team. A support dashboard may tolerate visibly stale counts, while a fulfillment instruction may require a confirmed transition before an operator acts. Record the affected task, maximum acceptable uncertainty as a locally chosen requirement, and the evidence returned when that requirement cannot be met. Eventual convergence is not a complete description of the user's experience.

16. Calculate whether a rebuild can catch up

Consider an illustrative projection with ten million unprocessed events when a rebuild starts. Assume new events arrive at five hundred per second and the isolated worker can apply two thousand five hundred per second, including the ongoing arrivals. Under a constant-rate model, net backlog reduction is two thousand events per second. Ten million divided by two thousand is five thousand seconds, or eighty-three minutes and twenty seconds. During that interval, two and a half million new events arrive, so the worker processes twelve and a half million events in total.

These figures are assumptions for capacity planning, not a benchmark or a recovery promise. The model excludes startup, checkpoint overhead, index construction, reconciliation, throttling, failures, and changes in event cost. It also assumes that available worker capacity does not come at the expense of the command service. A rebuild competing for the same storage I/O can slow live traffic and reduce its own expected throughput.

| Worksheet input | Record from the actual rehearsal | | --- | --- | | Initial backlog | Relevant event count at the recorded source boundary | | Arrival rate | Sustained and burst arrivals in the same event units | | Apply rate | Measured end-to-end committed projection progress, not fetch rate | | Net progress | Apply rate minus arrivals while both rates remain comparable | | Shared constraints | Storage I/O, network, locks, memory, and downstream query load | | Completion work | Reconciliation, index readiness, catch-up, and query cutover |

If apply rate is no greater than arrival rate, this model never catches up. More elapsed time does not solve the deficit. The options include improving the reducer or indexing strategy, adding capacity where ordering permits, narrowing the projection, or arranging an explicitly approved write restriction. Splitting workers cannot parallelize a single ordered stream arbitrarily. A hot stream or expensive event type can dominate even when aggregate throughput appears adequate.

Budget retained event storage and temporary projection storage separately. The shadow model may coexist with the current model, its indexes, backup copies, and an earlier recovery version. A cost estimate that includes only steady-state event writes will understate this transition. Record storage growth, restore throughput, and deletion policy for abandoned builds. Delete a failed projection only after establishing that it is derived, isolated, and no longer needed as evidence or a recovery option.

17. Rebuild beside live queries, not over them

The second view separates two questions: where committed events travel, and which model is allowed to answer production queries. A shadow reducer consumes retained history and then live changes, but the query alias continues to point at the accepted model until reconciliation and freshness checks pass. The alias represents a routing choice, not a distributed transaction and not a replacement for access controls.

Compare models at compatible source positions. Comparing the latest live result with a shadow result several minutes behind will produce differences that are neither bugs nor proof of correctness. For deterministic fields, compare exact values at a controlled boundary. For derived values whose semantics intentionally changed, define a reviewed reconciliation rule and representative edge cases. A total row count is insufficient when the stock balance for one item is wrong.

Stop the candidate on unknown event semantics, unexplained invariant differences, missing source intervals, or a security-boundary failure. Preserve its checkpoint, reducer version, input identifiers, and diagnostic context. Avoid copying unrestricted event payloads into an incident ticket. Decide whether to repair the reducer and restart, resume from a safe checkpoint, or abandon the build. Do not advance past a poisoned event just to make the lag dashboard green.

For cutover, the service owner records the accepted model version, source positions, reconciliation evidence, and the query-routing change. Retain the prior model only if it can remain a valid recovery target. If it stops receiving events, switching back later may return stale data. If a new event type cannot be handled by the old reducer, an apparently preserved model may no longer be recoverable without another change. Document that boundary before adopting the new event contract.

18. Operational and security consequences of retained history

Read access to event history can reveal more than access to the current application state. A current contact field may be hidden while an older event still contains its previous value. A replay account may cross tenant boundaries while an interactive service cannot. Enumerate command writers, event readers, projection writers, query users, operators, backup restorers, and export jobs as separate capabilities. A shared administrator credential makes those boundaries difficult to demonstrate.

Treat rebuild authorization as a time-bounded operational change. Record who approved the source scope, destination model, reducer artifact, network permissions, and expiration of temporary access. Use separate credentials for any effect executor, and test that the shadow worker cannot obtain them. Storing a flag named replay does not prevent a misconfigured process from sending a message or invoking an integration.

The diagnostic record should identify the event and processing context without automatically duplicating its entire payload. Record safe identifiers, version, checkpoint, reducer artifact, and failure category; protect any required forensic payload separately. The OWASP Logging Cheat Sheet provides guidance on excluded data, access restrictions, and testing logging failures. A support export and a dead-letter store require the same data-lifecycle attention as the primary event store.

Retention changes are also operational changes. Before removing history, enumerate the projections, snapshots, correction workflows, investigations, and recovery procedures that depend on it. Rehearse the permitted reconstruction path from the retained boundary. If a snapshot becomes the only remaining representation of older state, protect and version it as required recovery data. Record what historical questions can no longer be answered. This is a technical capability statement; legal sufficiency requires qualified review of the actual obligations and all copies.

19. Review checklist and adoption decision

The architecture review should end with an owned, bounded decision. The domain owner explains the historical question and accepts the read-after-write behavior. The storage owner supplies concurrency, receipt, and recovery evidence. The projection owner demonstrates checkpoint safety, unknown-event handling, and reconciliation. Security and privacy owners review access and lifecycle controls. The service owner accepts the runbooks and recurring maintenance cost.

Ask each owner for one concrete counterexample that would invalidate the design. A missing historical input can invalidate reconstruction. A receipt that expires before a supported retry can invalidate duplicate protection. A projection that cannot catch up can invalidate the promised recovery window. A restored backup without usable keys can invalidate recoverability. These are better review questions than asking whether the architecture follows an event-sourcing pattern correctly in the abstract.

Proceed only for the named context and the evidence demonstrated. Keep an adoption record with the alternatives rejected, unresolved limitations, test artifacts, product versions, and a review trigger such as a new event family or a changed retention requirement. If the historical requirement does not justify those obligations, retain ordinary transactional state and add the specific audit or reporting capability that is actually needed. Microsoft's event-sourcing pattern guidance likewise treats adoption as a tradeoff, not a default destination.

Limitations and next step

The architecture is a proposed pattern, not a tested production system. The executable fixture cannot establish transactional durability or end-to-end exactly-once effects. No bank, healthcare, retail, or other client association is asserted.

Publication requires a named distributed-systems reviewer, a selected store and version, actual atomicity and recovery tests, and a reviewed data-lifecycle design. Future case claims require provenance, measurement context, and permission. Editorial label: Ampity Editorial Review. Primary references were checked on September 21, 2026; this documentation check is not named human technical approval.

This paper owns operational event-history and replay boundaries. General architecture selection and database migration deserve separate treatment; overlapping titles do not authorize a merge or redirect.

For a scoped design and implementation review, see backend systems and APIs. For the broader consistency and ownership decision, see system architecture design.

Primary references