Event Replay for Event-Sourced Systems
Event replay reads immutable events again in their original stream order and applies them to a new or reset consumer. Use replay to rebuild projections, reproduce historical state, test new reducers, or backfill derived outputs—never to re-trigger uncontrolled external side effects.
Problem
Why this pattern exists
Replay is powerful because recorded facts become reusable input. It is dangerous when a handler mixes deterministic state reduction with sending email, charging cards, or calling mutable services. Reprocessing old events can duplicate real-world effects unless replay context and idempotency boundaries are explicit.
Production replay also competes with live traffic and may encounter schemas written years ago. A safe plan defines source range, event filters, target consumer version, rate limits, validation sample, catch-up behavior, and cutover. Historical payloads must be decoded through compatible upcasters rather than rewritten in place.
Design decisions
Make boundaries explicit
- 01
Separate projection from effect
Keep pure state fold independent from outbound effect dispatcher. Replays can rebuild state while effect handlers ignore replay frames or deduplicate by event ID.
- 02
Replay into new target
Build projection or export under versioned name. Avoid destroying current serving view until replacement catches up and passes comparison checks.
- 03
Bound and observe workload
Define start and end coordinates, throughput budget, lag target, error policy, and pause control. Replay is an operation with progress, not opaque background loop.
AllSource implementation
Apply pattern to durable Core history
AllSource stores ordered events in Core and exposes query, reconstruction, snapshot, and durable-consumer paths. For a projection rebuild, register new consumer identity, replay from its initial cursor, fold events deterministically, then continue into live WebSocket delivery. Existing view stays available during catch-up.
For incident analysis, query one entity and reconstruct state at timestamp before failure. Compare event IDs, versions, payloads, and metadata with current state; this answers what system knew then without editing history. For large backfills, filter to required event namespaces, throttle processing, and measure cursor lag against Core head before switching readers.
target: account_balance_v3
source: tenant-scoped Core events
range: position 0 -> captured live head
effects: disabled
validation: totals, entity count, sampled state hashes
cutover: after v3 catches live stream and checks passFailure modes
Detect weak implementations early
Historical replay sends duplicate notifications or payments.
Fix: Separate pure folds from effects and deduplicate effects by event ID.
Rebuild overwrites working projection before validation.
Fix: Replay into versioned target and use explicit cutover.
Old payload cannot be read by current code.
Fix: Add deterministic schema upcaster; preserve original stored event.
Production checklist
Ready when each statement is true
- Replay range, filters, target, and owner are explicit.
- External side effects are disabled or idempotent.
- Historical schemas have deterministic read compatibility.
- Progress, throughput, lag, failures, pause, and resume are observable.
- New output is verified before reader cutover.
Related patterns
Continue through adjacent decisions
Projections and read models
A projection folds ordered events into a query-specific read model. It is derived state, not a second source of truth: operators must be able to discard it, replay source events, and build a replacement without rewriting event history or stopping existing readers.
Read nextIdempotent consumers
An idempotent event consumer produces the same durable result when it receives an event more than once. Use stable event IDs, entity versions, unique effect keys, and acknowledge only after processing succeeds; at-least-once delivery then becomes recoverable instead of corrupting state.
Read nextEvent schema evolution
Evolve event schemas by preserving stored facts, registering versioned contracts, making compatible additive changes, and translating old payloads at read time when semantics change. Never rewrite production history merely to match newest application model.
Read nextStore history once
