All event-sourcing patterns
Production pattern447 words · verified 10 September 2026

Optimistic Concurrency in Event Sourcing

Optimistic concurrency protects a stream by accepting a write only when its expected version matches the current version. A mismatch means another command changed the aggregate first, so the caller must reload events, re-evaluate the command, and either append a new valid event or report a domain conflict.

Problem

Why this pattern exists

Concurrent writers can both read version 12, make decisions from the same state, and attempt version 13. Without a compare-and-append guard, both facts may enter history even when they violate an invariant such as spending the same balance twice or confirming an already-cancelled booking.

A conflict is not a generic transport failure. Blindly retrying the same payload can preserve an invalid decision. Correct handling depends on command semantics: some operations can be recalculated, some are naturally idempotent, and others must return a business-level conflict for a person or upstream workflow to resolve.

Design decisions

Make boundaries explicit

  1. 01

    Read with version

    Load the aggregate's ordered events and retain latest version alongside derived state. Command handling must carry that version into append; dropping it creates a time-of-check/time-of-use gap.

  2. 02

    Compare at commit

    Validate expected version inside event-store write path, not in application code before network transit. Only storage layer can make comparison and append one atomic decision for stream.

  3. 03

    Retry decisions, not writes

    After mismatch, reload intervening events and run domain decision again. Reusing stale event payload is safe only when operation is explicitly commutative and idempotent.

AllSource implementation

Apply pattern to durable Core history

AllSource Core supports expected-version checks in its ingest path. Build aggregate state from tenant-scoped stream, capture current version, then append with that expectation. Core rejects a mismatch before adding event. Record command ID or idempotency key in metadata so network retries can be distinguished from new business commands.

Instrument conflict rate by event type and stream. A sudden rise often signals aggregate boundary that is too broad, a client holding state too long, or duplicate delivery without idempotency. Do not hide persistent conflicts behind infinite retry loops; cap automatic retries, add jitter for transient contention, and surface invariant failures distinctly from version mismatches.

Compare-and-append decision
read account-42       -> version 18, available = 5000
decide withdraw 2000    -> account.withdrawal_requested
append expected v18     -> accepted as version 19

second append expects 18 -> concurrency error
reload version 19        -> re-evaluate available balance

Failure modes

Detect weak implementations early

Two conflicting commands both become accepted facts.

Fix: Require expected version on invariant-sensitive appends.

Client retries stale event until it eventually appends.

Fix: Reload and rerun domain decision after every version mismatch.

Unrelated commands conflict on a high-traffic global stream.

Fix: Narrow aggregate boundary and move cross-stream views into projections.

Production checklist

Ready when each statement is true

  • Reads return aggregate version with derived state.
  • Invariant-sensitive appends include expected version.
  • Conflict and duplicate-command errors are separate.
  • Retries rerun decision logic against fresh history.
  • Conflict metrics identify hot streams and poor boundaries.

Related patterns

Store history once

Rebuild every useful view from durable events.