Event Sourcing and CQRS: Concurrency, Replay and Production Tradeoffs
Decide whether event sourcing and CQRS fit your domain, then plan concurrency checks, event-store requirements and a projection rebuild that cannot repeat external...
Three decisions, not one architecture package
Event sourcing makes an ordered history of domain events the authoritative record from which state is derived. CQRS separates the models used to change state from those used to read it. Event-driven architecture describes communication through events.
You can use any of these without automatically adopting the others. A conventional database can publish integration events. CQRS can use one database with separate read and write logic, as Microsoft's CQRS guidance explains. A broker is not mandatory.
Choose event sourcing when preserving domain decisions and rebuilding alternative views are important enough to justify long-lived event contracts and recovery work. Do not adopt it merely because services already publish messages. For reliable messaging without event-sourced state, use the event-driven architecture guide.
Check whether simpler history is sufficient
A current-state database does not make historical queries impossible. SQL Server's system-versioned temporal tables, for example, retain prior row versions for point-in-time queries. History tables and dedicated audit records provide other design options.
The difference is what the history represents. A row history can show that a quantity changed; a domain event can record that a reservation was accepted or released, with the relevant business context. Neither automatically captures every reason, access attempt or external fact.
| Need | Candidate approach | Question to settle | | --- | --- | --- | | See prior row values | Current state plus history | Does recorded system time answer the business question? | | Keep a security audit | Purpose-built audit records and controls | Are actor, access, integrity and retention needs covered? | | Rebuild domain state and new views | Event sourcing | Can the team retain readable events and operate replay safely? | | Different read and write models | CQRS, with or without events | Is the benefit worth extra model and sync work? |
An append-only design is not a compliance guarantee. Review access, authorized corrections, tamper detection, backups, retention and erasure requirements with the responsible security, privacy and legal owners. Avoid placing unnecessary personal data in permanent events. If data is stored separately and referenced, determine how later deletion affects historical reconstruction.
Also distinguish “what the system knew then” from “what the business now believes happened then.” A correction recorded today can refer to an earlier effective date. That requires explicit modeling, not simply sorting event timestamps.
Make one aggregate's rules enforceable
An aggregate is a boundary within which a command enforces a business invariant. Reconstruct its state, evaluate the command and append resulting events only if the stream has not changed since it was read.
The following is an illustrative concurrency test for one inventory aggregate. At revision 12, five units remain. Two commands arrive: reserve four units and reserve three.
| Step | Result | | --- | --- | | Both handlers read revision 12 | Each initially sees five available units. | | First handler appends a four-unit reservation, expecting revision 12 | Append succeeds at revision 13; one unit remains. | | Second handler appends a three-unit reservation, expecting revision 12 | Store rejects the stale expected revision. | | Second handler reloads and reevaluates | Three units cannot be reserved from the remaining one; the command is refused. |
Without a conditional append or another correctly enforced concurrency mechanism, both commands could pass their initial checks and reserve seven units from five. A retry after a conflict must reevaluate the business rule against new state. It must not simply change the expected revision and append the previously computed event.
Kurrent's Node client v1.3 documentation describes expected stream state and revision checks. Its documentation also distinguishes atomic append APIs by server version, including availability of appendRecords from KurrentDB 26.1. Confirm compatibility with the server and client versions you actually deploy; an example from a newer API is not a capability guarantee for an older cluster.
A command ID serves a different purpose from the stream revision. If the client times out after a successful append, the retry needs a reliable way to discover the prior command result instead of making another reservation. Specify how command identity and result are committed or recovered within the authoritative write boundary.
One aggregate's revision does not protect a rule spanning several aggregates. Use a suitable transactional boundary or an explicit process with intermediate states and compensation. Do not imply that local optimistic concurrency creates a distributed transaction.
Evaluate the event store as a system of record
A purpose-built event database, a carefully designed transactional database or a log-based design can support different parts of the solution. Product category alone does not establish the contract.
Require evidence for these capabilities:
- Conditional append or an equivalent mechanism that prevents conflicting aggregate writes.
- Atomic persistence of the event batch required by one accepted command.
- Efficient, ordered reads of the stream needed to reconstruct an aggregate.
- Durable acknowledgements, backup and restore behavior under the chosen deployment settings.
- Retention that preserves the history needed for reconstruction and audit requirements.
- Resumable subscriptions or another reliable way to drive projections.
- Access controls, schema evolution and an operationally tested recovery path.
Kafka provides an ordered log within each partition, but partition order is not an expected-revision check for a particular aggregate. A Kafka-based source of truth needs an explicit writer/concurrency design, adequate stream-read access and a retention policy that does not discard required history. Log compaction by an aggregate key can remove older records needed for replay. Do not treat a default topic as a drop-in event store.
The Kafka 4.0 design reference is useful for checking log and transaction semantics. Verify the matching documentation and settings for the selected broker release. A separate index or snapshot can help reads, but it creates another component whose loss and rebuild behavior must be understood.
Close the append-to-publication gap
Once the authoritative event append succeeds, the command is accepted even if a projection is behind. Publishing a second copy to a broker must not be an unrelated best-effort step that can be lost after the append.
Use a resumable subscription from the authoritative log, or commit an outbox in the same transaction where that design is supported. Record progress durably and expect duplicates after a crash. Projection changes and their checkpoint should commit atomically when they share a store; otherwise design and test equivalent deduplication and recovery.
Do not advance the checkpoint before its state changes are durable. That can mark unprocessed work as complete. If the output commits first and the checkpoint is lost, replay must not double-apply an increment.
For user-facing reads, decide how staleness is exposed. A command can return its accepted revision, and a read path can wait within a bounded deadline for a projection that has reached it, or show a pending state. Do not claim that an accepted command is visible in every view immediately.
Worked recovery: rebuild a view, not external actions
Suppose a new projection contains a bug. Keep its rebuild separate from live handlers that send email, charge a payment or provision a resource. Reconstructing state should apply stored facts deterministically; it should not execute the original commands again.
For this hypothetical single ordered input, the old view remains available while a replacement is built:
| Stage | Recovery rule | | --- | --- | | Start | Create a separate read store and a checkpoint for the new projection version. | | Resume | If the last durable checkpoint is 420, continue with event 421. | | Fail at 433 | Keep the checkpoint at 432; retain the error and event identity for review. | | Repair | Fix the handler or approved schema adapter, test the case and retry 433. | | Catch up | Process retained history and new arrivals; compare both views at the same recorded position. | | Switch reads | Route to the verified replacement only after its required position and checks pass. |
The position is illustrative. A partitioned source may require a checkpoint per partition and additional ordering rules. A global integer cannot be assumed.
Quarantining event 433 and continuing may be valid for an independent notification, but not for a projection whose later calculations depend on it. Do not silently skip it and label the view complete. A temporary exception needs an owner, a visible gap and a repair plan.
Replay code should have no credentials for external-effect destinations. A “replay mode” flag alone is a weak boundary if the wrong handler can still run. Live side-effect consumers need their own durable operation identity, destination idempotency where available and reconciliation for uncertain outcomes.
Treat old events as a supported interface
Keep old schema fixtures and test current handlers against them. An upcaster translates an old stored shape to the shape a reader expects; it should not silently rewrite the historical business meaning. Record policy inputs needed to explain past decisions rather than recomputing them from today's prices or rules.
Snapshots can reduce aggregate loading work. Include the stream revision and state/schema version so the remaining events can be applied correctly. A snapshot that cannot be trusted should be disposable if the retained history remains sufficient. If history has been removed, the snapshot may have become essential state and needs a different recovery contract.
Microsoft's event-sourcing guidance emphasizes that the pattern adds substantial long-term tradeoffs. Make those costs part of the decision: replay duration, projection lag, schema support, operational access and the effort of moving away later.
Before committing, demonstrate the stale-writer test, a timed-out command retry, an append-to-publish crash and a projection rebuild with an invalid event. If these exercises expose unclear ownership, bring the failure traces and the business invariant to Ampity's system architecture and design service. A broker diagram is not enough to establish that the operating model is ready.