On This Page
1The Problem It Solves2Pattern Structure
3When to Use4When Not to Use
5Trade-offs6Implementation Approach
7Anti-Patterns to Avoid8Cloud-Specific Implementations
9References

The Problem It Solves

Traditional data storage keeps only the current state. You know that an order is cancelled but not that it was first placed, then modified, then disputed, then cancelled. Audit trails are afterthoughts added alongside the current state table. Debugging production issues requires log correlation rather than replaying the sequence of events that led to the problem.

Event sourcing makes the history of state changes the primary data model. The current state is a derived view, not the authoritative record.

Pattern Structure

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'Inter, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD START([Command Received]) START --> LOAD[Load Aggregate\nReplay events from event store\nApply each event to build state] LOAD --> VALIDATE_ES[Apply Business Rules\nValidate command against\ncurrent aggregate state] VALIDATE_ES --> RESULT{Command\nValid?} RESULT -->|No| REJECT[Reject command\nReturn domain error\nNo event written] RESULT -->|Yes| APPEND[Append new event\nto event store\nImmutable, ordered, timestamped] APPEND --> NOTIFY[Notify subscribers\nProject to read models\nTrigger downstream sagas] NOTIFY --> SNAPSHOT{Aggregate event\ncount exceeds threshold?} SNAPSHOT -->|Yes| SNAP_WRITE[Write snapshot\nCurrent state at event N\nSpeedup future replays] SNAPSHOT -->|No| DONE_ES([State Updated via Event Log]) SNAP_WRITE --> DONE_ES style START fill:#4f8ef7,color:#fff style DONE_ES fill:#10b981,color:#fff style REJECT fill:#fef3c7 style SNAP_WRITE fill:#e0f2fe

When to Use

  • Domains requiring a complete, auditable history of all state changes — finance, healthcare, legal
  • Systems where the ability to replay events to rebuild state or debug issues has significant operational value
  • Architectures already using CQRS — event sourcing provides the event stream that powers read model projections
  • Systems requiring temporal queries — what was the state of this order at 14:32 last Tuesday?
  • Event-driven architectures where the event log also serves as the integration bus between services

When Not to Use

  • Simple CRUD domains where state history has no business value
  • Teams new to event sourcing — the learning curve is steep and the operational model differs significantly from traditional databases
  • Systems requiring complex reporting queries against current state — pure event sourcing makes ad-hoc queries difficult without projections
  • High-frequency, high-volume state changes where replay time becomes impractical without aggressive snapshotting

Trade-offs

Benefit Cost
Complete audit trail by design — no separate audit table Reading current state requires replaying events — mitigated by snapshots
Temporal queries — reconstruct state at any point in time Changing event schemas after events are written is complex
Natural integration event stream — events published to other services Steeper learning curve for teams new to the pattern
Bug reproduction — replay the exact event sequence that caused the issue Event store becomes large over time — archival and retention policies needed

Implementation Approach

Design events as facts, not deltas. An event describes what happened — OrderPlaced, ItemAdded, PaymentProcessed — not a diff of what changed. Events are named in the past tense. They carry all the data needed to understand what occurred without referencing external state.

Aggregate identity determines the event stream. All events for a given order are stored in the same stream, keyed by order ID. Loading the aggregate means reading all events in that stream in order. The aggregate applies each event to rebuild its current state.

Implement snapshotting for long-lived aggregates. After N events (typically 50–200), write a snapshot of the current state. Future loads start from the most recent snapshot and replay only the events after it. This bounds the replay time regardless of how long-lived the aggregate is.

Treat the event store as append-only infrastructure. Events are never modified or deleted after writing. Schema changes are handled through event versioning and upcasting — transforming older event formats to the current schema on read.

Anti-Patterns to Avoid

⚠ 1. Mutable Events

Modifying or deleting events after they have been appended to the store to correct a mistake. The event store's value comes entirely from its immutability and completeness. Mutating history undermines the audit trail and breaks any projection built from those events.

Hover to see the fix ↻
↺ Correct Approach

Append a compensating event — OrderCancelled corrects an OrderPlaced that should not have occurred. The log reflects what actually happened including the correction.

⚠ 2. Events as Commands

Designing events that describe what should happen rather than what did happen. PlaceOrder is a command. OrderPlaced is an event. Storing command-shaped events means replaying them re-executes business logic, producing different results as business rules change.

Hover to see the fix ↻
↺ Correct Approach

Events describe facts about the past. They are named in the past tense and contain the data that was true at the moment the event occurred — not instructions for what to do with that data.

Cloud-Specific Implementations

  • AWS: DynamoDB Streams provides a 24-hour ordered event log per table — a practical event sourcing store for most workloads. Run EventStoreDB on ECS Fargate for a dedicated event store. See Event-Driven Architecture on AWS for the EventBridge + Lambda consumer pattern.

Flowchart

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'Inter, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD START([Command Arrives]) START --> LOAD_ES[Load Aggregate\nRead event stream from store\nApply events to rebuild state] LOAD_ES --> SNAP_Q{Snapshot\nAvailable?} SNAP_Q -->|Yes| USE_SNAP[Load snapshot\nReplay events since snapshot\nFaster than full replay] SNAP_Q -->|No| FULL_REPLAY[Full event replay\nFrom stream beginning] USE_SNAP & FULL_REPLAY --> DECIDE{Business Rule\nSatisfied?} DECIDE -->|No| REJECT_ES[Reject — return error\nNothing written to store] DECIDE -->|Yes| WRITE_ES[Append Event to Store\nImmutable fact\nPast tense — OrderPlaced] WRITE_ES --> PROJECT_ES[Update Projections\nRead models rebuilt\nFrom new event] WRITE_ES --> PUBLISH_ES[Publish Integration Event\nOther services notified\nSaga steps triggered] PROJECT_ES --> QUERY_ES[Query Handler\nReads from projection\nFast denormalised view] QUERY_ES --> RESPONSE_ES([Current State Available]) style START fill:#4f8ef7,color:#fff style RESPONSE_ES fill:#10b981,color:#fff style REJECT_ES fill:#fef3c7 style USE_SNAP fill:#e0f2fe

References

  1. Young, Greg — Event Sourcing. eventuate.io/exampleapps.html
  2. Fowler, Martin — Event Sourcing Pattern. martinfowler.com/eaaDev/EventSourcing
  3. Vernon, Vaughn — Implementing Domain-Driven Design. Addison-Wesley, 2013.
  4. EventStoreDB — Purpose-built event sourcing database. eventstore.com
Ascendion Engineering Knowledge Base ← Data Patterns