Event Sourcing Pattern: Auditability, Replay, and Evolution of Domain State
Why Storing Events Instead of State Changes Everything
Event sourcing pays off when regulatory audit history and replay are first-class requirements β but it demands strict schema evolution, a snapshot strategy, and a framework that owns aggregate lifecycle.
π― System Scenario & Requirements
System Goal
Event sourcing pays off when regulatory audit history and replay are first-class requirements β but it demands strict schema evolution, a snapshot strategy, and a framework that owns aggregate lifecycle.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: Event sourcing pays off when regulatory audit history and replay are first-class requirements β but it demands strict schema evolution, a snapshot strategy, and a framework that owns aggregate lifecycle.
- Spring Boot + Axon Framework is the fastest production-grade path on the JVM.
- In 2017, a GitLab database administrator ran rm -rf on the wrong production server.
Constraints & Assumptions
β‘ Execution Stages & Decision Trace
Stage 1 β The Four Building Blocks of an Event-Sourced System
Every production event-sourced system has four roles: 1.
Command β an intent to change state; validated against current aggregate state before writing.
Deep Dive
Scenario context
Event sourcing pays off when regulatory audit history and replay are first-class requirements β but it demands strict schema evolution, a snapshot strategy, and a framework that owns aggregate lifecycle. Spring Boot + Axon Framework is the fastest production-grade path on the JVM.
In 2017, a GitLab database administrator ran rm -rf on the wrong production server. They had no event log β just nightly snapshots. Siβ¦
This walkthrough focuses on Why Storing Events Instead of State Changes Everything.
Before making a design choice, clarify:
- In 2017, a GitLab database administrator ran rm -rf on the wrong production server.
- They had no event log β just nightly snapshots.
- Six hours of user data was lost permanently, and thousands of repositories were irrecoverable.
Concepts in play: event-driven-architecture, jvm-internals-and-garbage-collection, consistency-replication, design-principles-solid
Stage 1 β The Four Building Blocks of an Event-Sourced System
- Every production event-sourced system has four roles: 1.
- Command β an intent to change state; validated against current aggregate state before writing.
- Aggregate β the consistency boundary; enforces invariants, emits events, and advances its internal state machine.
- Event Store β the append-only log; events are immutable, each aggregate instance owns a stream by ID.
Observable outcome: Event Store β the append-only log; events are immutable, each aggregate instance owns a stream by ID.
Stage 2 β How a Command Flows into an Auditable Event Stream
- Solid arrows show the live command path.
- Dashed arrows show replay β the TrackingEventProcessor resets its token to reconstruct the query model for audit at any historical timestamp.
- The aggregate never writes directly to the query model.
- It emits events; projections consume them independently.
flowchart TD
C[Client Command] --> CH["Command Handler (SubscriptionAggregate)"]
CH -->|"validates invariants applies event"| ES[(Event Store Append-Only Log)]
ES -->|"event published on event bus"| P["BillingHistoryProjection (Event Handler)"]
P --> QM[(Query Model BillingHistoryRepository)]
QM -->|"query response"| Q[GetBillingHistoryQuery]
ES -. "token-based replay" .-> RP["Replay Processor (TrackingEventProcessor)"]
RP -. "rebuilds view for audit dispute" .-> QM
style ES fill:#f5f5f5,stroke:#555
style QM fill:#e8f4e8,stroke:#555
style RP fill:#fff3e0,stroke:#f90,stroke-dasharray: 5 5
Observable outcome: It emits events; projections consume them independently.
Stage 3 β Deep Dive: Inside the Aggregate: State Machines, Snapshots, and Schema Evolution
- Internals: Aggregate State Reconstruction An aggregate's state exists only in memory during command processing.
- Before handling a command, the framework loads the aggregate by replaying every past event for that aggregate ID in sequence.
- Each @EventSourcingHandler method advances internal state β status flags, counters, IDs β until the aggregate is fully current.
- The command handler then checks invariants against that reconstructed in-memory state.
Observable outcome: The command handler then checks invariants against that reconstructed in-memory state.
Stage 4 β Axon Framework and EventStoreDB: Event Sourcing on the JVM
- Axon Framework is a Spring Boot-native Java framework that manages the full event-sourcing lifecycle: aggregate command handling, event persistence, snapshotting, replay, upcasting, and projection tracking.
- EventStoreDB is a purpose-built append-only database with server-side projections and persistent subscription support β the recommended backend for production Axon deployments requiring audit-grade storage.
- These tools solve the event-sourcing problem by owning the infrastructure that makes aggregates deterministic: Axon's @CommandHandler / @EventSourcingHandler pattern enforces the strict separation between command validaβ¦
- Teams write domain logic; Axon owns the replay machinery.
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>4.9.3</version>
</dependency>
<!-- Optional: EventStoreDB connector replaces the default JPA event store -->
<dependency>
<groupId>org.axonframework.extensions.eventstored</groupId>
<artifactId>axon-eventstoredb-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
Observable outcome: Teams write domain logic; Axon owns the replay machinery.
Stage 5 β Real-World Applications
- Event sourcing earns its complexity where audit trails and replay are first-class business requirements.
Observable outcome: Event sourcing earns its complexity where audit trails and replay are first-class business requirements.
Stage 6 β Subscription Billing: Building the Aggregate, Projection, and Replay
- Scenario: A billing platform tracks the lifecycle of each subscription β CREATED β ACTIVATED β SUSPENDED β CANCELLED .
- Every state transition is an immutable domain event appended to the subscription's event stream.
- When a customer disputes a charge, the support team replays the event stream to reconstruct exactly what the account looked like at the moment of the disputed transaction.
- Maven Dependency Domain Events (Immutable Value Objects) Each event is a value object with no setters.
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>4.9.3</version>
</dependency>
public record SubscriptionCreatedEvent(
String subscriptionId, String tenantId, String planId, Instant occurredAt) {}
public record SubscriptionActivatedEvent(
String subscriptionId, String tenantId, Instant occurredAt) {}
public record SubscriptionSuspendedEvent(
String subscriptionId, String tenantId, String reason, Instant occurredAt) {}
public record SubscriptionCancelledEvent(
String subscriptionId, String tenantId, String reason, Instant occurredAt) {}
Observable outcome: Maven Dependency Domain Events (Immutable Value Objects) Each event is a value object with no setters.
End-to-end outcome
In 2017, a GitLab database administrator ran rm -rf on the wrong production server.
Alternatives considered
Decision Guide: When Event Sourcing Earns Its Complexity
Decision Guide: When Event Sourcing Earns Its Complexity
Failure and degraded modes
Trade-offs & Failure Modes in Practice
- Detection: Trade-offs & Failure Modes in Practice
- User or operator impact: Trade-offs & Failure Modes in Practice
