Build an Event Sourcing and CQRS Audit Trail


databases microservices reliability

System Design Deep Dive

Event Sourcing and CQRS Audit Trail

How do you replay 3 years of history when your database only stores current state?

⏱ 14 min read 📐 Advanced 🏗️ Architecture

Think of a traditional database like a whiteboard. When someone updates a record, they erase what was there and write the new value. The history is gone - you only have today’s truth. Now imagine instead you kept a notebook where every change was written as a new line, and the current state was just the sum of all those lines. That is event sourcing. It is not a clever pattern or an optimization trick - it is a fundamentally different model for persisting application state.

The challenge is scale. A financial trading platform generates millions of state changes per day. An e-commerce system creates order events, payment events, fulfillment events, and return events for every transaction. Over 3 years, that is billions of events. Replaying them to reconstruct any past state cannot be slow - a compliance audit that takes 6 hours to reconstruct last quarter’s state is not useful. And on the query side, the same event stream needs to power dashboards that ask completely different questions: “What is the current balance?” and “What were all the state changes for account X on March 15th, 2024?” are structurally incompatible queries if served from the same model.

The CQRS (Command Query Responsibility Segregation) pattern addresses this by separating writes from reads. Commands mutate state by appending events. Queries read from projections - denormalized views built specifically for each query shape. This sounds simple until you realize projections can fall behind, can fail mid-build, and must be rebuildable from scratch on demand. Add in snapshot optimization to avoid replaying 3 years of history on every read, and you have a system with six interacting layers that each fail in interesting ways.

We need to solve for durable ordered event storage, efficient aggregate reconstruction, projection consistency, snapshot management, and schema evolution simultaneously.

Requirements and Constraints

Functional Requirements

  • Append events to named aggregates (e.g., order:123, account:456) with optimistic concurrency control
  • Reconstruct any aggregate’s state at the current point in time by replaying its events
  • Reconstruct any aggregate’s state at any arbitrary past point in time (point-in-time queries)
  • Build and maintain read-optimized projections from the event stream
  • Support replaying the entire event stream from scratch to rebuild projections
  • Detect and handle conflicting concurrent writes (two processes updating the same aggregate)

Non-Functional Requirements

  • Event append latency: under 10ms at p99
  • Aggregate load latency: under 50ms for aggregates with up to 10,000 events
  • Replay throughput: at least 500,000 events/second for projection rebuild
  • Storage: retain all events for 3 years minimum (approximately 1 billion events at 1KB average = 1TB)
  • Availability: 99.99% for writes; 99.9% for projection queries
  • Event ordering: strict ordering within a single aggregate; best-effort global ordering across aggregates

Constraints

  • Events are immutable once written - no updates or deletes to the event log
  • Schema evolution must be handled in the application layer, not the store
  • Cross-aggregate transactions are out of scope - the system provides per-aggregate consistency only
  • Projections are eventually consistent with the event stream - not immediate

High-Level Architecture

The system splits into three planes: the write plane that accepts commands and appends events, the read plane that serves queries from projections, and the projection engine that transforms the event stream into queryable views.

Event sourcing CQRS system architecture overview

When a command arrives, the command handler loads the current aggregate state, validates the command against that state, and if valid, appends one or more events to the event store. The event store is the single source of truth - an append-only log partitioned by aggregate ID. After appending, the event store publishes events to an event bus (Kafka or similar), which feeds multiple projection workers. Each projection worker maintains one read model - an order history projection, a balance projection, an audit trail projection - and writes to a read store optimized for that query shape. Queries hit the read store directly, never the event store.

The snapshot service periodically computes aggregate state at a known event sequence number and stores it as a checkpoint. On aggregate load, the system reads the latest snapshot and replays only the events after it, bounding replay time regardless of aggregate age.

Key Insight

The event store is the only component that needs to be strongly consistent. Everything else - projections, snapshots, read models - can be eventually consistent, which is what makes this architecture horizontally scalable.

The Event Store

The event store’s job is deceptively simple: append events in order, with no gaps, under concurrent writers.

Premium Content

Unlock the full article along with everything else in the archive — all in one place.

In-depth analysis Expert insights Full archive access
Unlock Full Article