Introduction

An Event Replay System lets teams reprocess historical events (for bug fixes, model retraining, backfills, or new consumers) while keeping the production system and live users unaffected. Replaying events is a powerful tool — but it can be dangerous if done naively: you can duplicate side-effects, corrupt read models, or trigger external integrations twice.

This article provides a practical, senior-developer guide to design and implement a safe event replay system that supports:

The architecture assumes an event store (Kafka, EventStoreDB, Azure Event Hubs, or a durable SQL-backed log). Examples will use Kafka-style terminology but patterns apply to other stores.

Goals and Non-Goals

Goals

Non-Goals

High-Level Architecture

┌──────────────┐        ┌───────────────┐        ┌──────────────┐
│ Event Store  │ ─────> │ Replay Worker │ ─────> │ Replay Sink  │
│ (Kafka/ESDB) │        │ (.NET Service)│        │ (Read Model) │
└──────────────┘        └──────┬────────┘        └──────┬───────┘
                                 │                       │
                                 ▼                       ▼
                            ┌───────────┐           ┌────────────┐
                            │Control API│ <-------> │Angular UI  │
                            └───────────┘           └────────────┘

Key Concepts

Isolated Replay

Events for replay should not be published to production sinks. Replay sinks include:

Idempotency

Replay must be idempotent. Techniques:

Side-Effect Management

Side-effects (emails, payments, third-party API calls) are managed by:

Transformation & Versioning

Events may need transformation:

Controlled Replay

Allow operators to:

Audit & Safety

Every replay run must be auditable:

Event Replay Flowchart

Start
  |
  v
Create ReplayRun (filters, mode, operator)
  |
  v
Validate Filters & Acquire Isolation Token
  |
  v
Open Dedicated Replay Consumer (read-only)
  |
  v
For each Event in stream (matching filter):
  |
  +--> If Event already processed (dedupe) -> skip
  |
  +--> Map/Transform Event (if required)
  |
  +--> Execute Handler in Replay Mode
         - If side-effects allowed? route to sandbox
         - Else suppress and log
  |
  +--> Write to Replay Sink (or Dry Run result)
  |
  +--> Commit checkpoint
  |
  v
End Loop
  |
  v
Mark ReplayRun Completed
  |
  v
End

Data Models

ReplayRun Table (SQL)

CREATE TABLE ReplayRun (
  ReplayRunId UNIQUEIDENTIFIER PRIMARY KEY,
  CreatedBy NVARCHAR(200),
  CreatedAt DATETIME2,
  StreamName NVARCHAR(200) NULL,
  FromOffset BIGINT NULL,
  ToOffset BIGINT NULL,
  FromTime DATETIME2 NULL,
  ToTime DATETIME2 NULL,
  Mode NVARCHAR(20) NOT NULL, -- DryRun | Replay | ReplayWithSideEffects
  Status NVARCHAR(20) NOT NULL, -- Created | Running | Paused | Cancelled | Completed | Failed
  ProcessedCount BIGINT DEFAULT 0,
  ErrorCount BIGINT DEFAULT 0,
  LastCheckpoint BIGINT NULL,
  ConfigJson NVARCHAR(MAX) NULL
);

ReplayEventLog (for dedupe & audit)

CREATE TABLE ReplayEventLog (
  Id BIGINT IDENTITY PRIMARY KEY,
  ReplayRunId UNIQUEIDENTIFIER,
  EventId UNIQUEIDENTIFIER,
  StreamOffset BIGINT,
  EventType NVARCHAR(200),
  ProcessedAt DATETIME2,
  Outcome NVARCHAR(20), -- Success | Skipped | Error
  Details NVARCHAR(MAX)
);
CREATE UNIQUE INDEX UX_ReplayEventLog_Run_Event ON ReplayEventLog(ReplayRunId, EventId);

Replay Worker Design (.NET)

Responsibilities

Worker Components

Partitioned Reading Example (Kafka)

.NET Pseudocode: PartitionWorker

public async Task RunAsync(PartitionRange range, CancellationToken ct) {
    var consumer = _kafka.CreateConsumer($"replay-{replayRunId}-{range.Partition}");
    consumer.Assign(new TopicPartitionOffset(topic, range.Partition, range.FromOffset));
    while (!ct.IsCancellationRequested) {
        var msg = consumer.Consume(_pollTimeout);
        if (msg == null) break;
        if (msg.Offset > range.ToOffset) break;

        // Dedupe: insert into ReplayEventLog unique; if fails => skip
        if (!await _replayEventLog.TryMarkProcessing(replayRunId, msg.EventId, msg.Offset)) {
           continue;
        }

        var evt = _transformer.Transform(msg.Value);
        var outcome = await _replayHandler.HandleAsync(evt, replayContext);
        await _replayEventLog.MarkProcessed(replayRunId, msg.EventId, outcome);
        await _checkpointStore.SaveAsync(replayRunId, range.Partition, msg.Offset);
    }
}

Ensuring Idempotency

Idempotent Write Patterns

Example Upsert (SQL Server)

MERGE INTO ReplayProjection AS target
USING (VALUES(@AggregateId, @Value, @EventVersion)) AS src(AggregateId, Value, EventVersion)
ON target.AggregateId = src.AggregateId
WHEN MATCHED AND target.EventVersion < src.EventVersion THEN
  UPDATE SET Value = src.Value, EventVersion = src.EventVersion
WHEN NOT MATCHED THEN
  INSERT (AggregateId, Value, EventVersion) VALUES (src.AggregateId, src.Value, src.EventVersion);

This ensures safe replays even when events are processed multiple times.

Side-Effect Handling Patterns

1. Suppress Mode (Default Safe)

2. Sandbox Mode

3. Replay-With-SideEffects (Controlled)

Transformation and Versioning

Control API and Angular UI

Control API (Endpoints)

Angular UI Components

Angular Sample: Start Replay

startReplay() {
  const payload = {
    streamName: this.form.stream,
    fromOffset: this.form.fromOffset,
    toOffset: this.form.toOffset,
    mode: this.form.mode,
    dryRun: this.form.dryRun
  };
  this.http.post('/api/replay', payload).subscribe(res => {
     this.router.navigate(['/replay', res.id]);
  });
}

UI must clearly signal that replay can be destructive if not run in safe mode.

Monitoring, Metrics, and Observability

Track:

Expose Prometheus metrics and logs. Correlate logs using ReplayRunId and EventId.

Security And Governance

Testing Strategy

Operational Playbook

  1. Pre-Run: Create a replay in DryRun mode; review EffectReview items.

  2. Approval: If safe, move to Replay mode or Replay-With-SideEffects with approval.

  3. Run: Start run in low-traffic window if necessary; throttle throughput.

  4. Monitor: Watch errors and processing rate. Pause or cancel on anomalies.

  5. Post-Run: Compare projection counts, compute diffs, run reconciliation scripts.

  6. Rollback Plan: For destructive projection updates, have a restore procedure (point-in-time restore of projection or snapshot before run).

Example: End-to-End Replay Scenario

Problem: A bug in a projection allowed incorrect tax calculation for orders between Jan 1–10. Fix is applied in projection code. Now replay events to correct projection without re-sending emails or payments.

Steps

  1. Fix handler code and deploy to a staging worker image tagged replay-v2.

  2. Create a ReplayRun with:

    • stream: orders

    • fromTime: 2025-01-01

    • toTime: 2025-01-10

    • mode: Replay (no side-effects)

    • operator: rajesh.gami

  3. Start replay in DryRun first. Inspect EffectReview (should show email sends suppressed).

  4. Review dry-run diffs for projection; confirm corrections expected.

  5. Run actual Replay. Projections are updated via upserts.

  6. Verify projection data and run reconciliation queries.

  7. Close run and archive logs.

No emails/payments were re-triggered because side-effects were suppressed.

Common Pitfalls And How To Avoid Them

Summary

An Event Replay System is a strategic capability for mature systems — enabling recovery, data correction, and re-computation without risking live users. The key pillars are: