Event Sourcing Pattern
Store the state of an application as a sequence of events rather than storing only the current state. An architectural pattern for audit-friendly systems.
Overview
The Event Sourcing Pattern stores the state of an application as a sequence of events rather than storing only the current state. Instead of updating a record in place, you append an event describing what happened. The current state is derived by replaying all events for an entity, which gives you a complete audit trail, temporal queries, and the ability to rebuild state at any point in time.
When to Use
Use the Event Sourcing Pattern when:
- You need a complete audit trail of every state change (finance, healthcare, compliance)
- You want to reconstruct historical states or debug by replaying events
- Event-driven architectures already exist, making event stores a natural fit
- CQRS is in use, and read models can be built from event projections
- You need to compensate for failures by replaying or reversing events (see Saga)
Typical fits: banking ledgers, inventory systems, order tracking, collaborative editing.
Skip it for plain CRUD. Event sourcing adds schema versioning, projections, and replay machinery that a small app never earns back — if nobody will ever ask “what happened and when”, a normal table is cheaper.
Solution
Python
from dataclasses import dataclass
from typing import Dict, List, Optional
from datetime import datetime
@dataclass
class Event:
type: str
entity_id: str
payload: dict
timestamp: Optional[str] = None
def __post_init__(self):
if self.timestamp is None:
self.timestamp = datetime.now().isoformat()
class EventStore:
def __init__(self):
self.streams: Dict[str, List[Event]] = {}
def append(self, entity_id: str, event: Event):
self.streams.setdefault(entity_id, []).append(event)
def get_stream(self, entity_id: str) -> List[Event]:
return list(self.streams.get(entity_id, []))
# Entity: rebuild state from events
class BankAccount:
def __init__(self, account_id: str):
self.account_id = account_id
self.balance = 0
self.version = 0
def apply(self, event: Event):
if event.type == "Deposited":
self.balance += event.payload["amount"]
elif event.type == "Withdrawn":
self.balance -= event.payload["amount"]
self.version += 1
@classmethod
def rehydrate(cls, account_id: str, events: List[Event]):
account = cls(account_id)
for e in events:
account.apply(e)
return account
# Usage
store = EventStore()
account_id = "ACC-123"
store.append(account_id, Event("Deposited", account_id, {"amount": 100}))
store.append(account_id, Event("Withdrawn", account_id, {"amount": 30}))
store.append(account_id, Event("Deposited", account_id, {"amount": 50}))
# Rebuild state
account = BankAccount.rehydrate(account_id, store.get_stream(account_id))
print(f"Balance: {account.balance}") # 120
# Full audit trail
for e in store.get_stream(account_id):
print(f"{e.timestamp}: {e.type} {e.payload}")
JavaScript
class Event {
constructor(type, entityId, payload) {
this.type = type;
this.entityId = entityId;
this.payload = payload;
this.timestamp = new Date().toISOString();
}
}
class EventStore {
constructor() {
this.streams = new Map();
}
append(entityId, event) {
if (!this.streams.has(entityId)) this.streams.set(entityId, []);
this.streams.get(entityId).push(event);
}
getStream(entityId) {
return this.streams.get(entityId) || [];
}
}
class BankAccount {
constructor(accountId) {
this.accountId = accountId;
this.balance = 0;
this.version = 0;
}
apply(event) {
if (event.type === "Deposited") this.balance += event.payload.amount;
if (event.type === "Withdrawn") this.balance -= event.payload.amount;
this.version++;
}
static rehydrate(accountId, events) {
const account = new BankAccount(accountId);
events.forEach(e => account.apply(e));
return account;
}
}
// Usage
const store = new EventStore();
const accountId = "ACC-123";
store.append(accountId, new Event("Deposited", accountId, { amount: 100 }));
store.append(accountId, new Event("Withdrawn", accountId, { amount: 30 }));
store.append(accountId, new Event("Deposited", accountId, { amount: 50 }));
const account = BankAccount.rehydrate(accountId, store.getStream(accountId));
console.log("Balance:", account.balance); // 120
// Audit trail
store.getStream(accountId).forEach(e =>
console.log(`${e.timestamp}: ${e.type}`, e.payload)
);
Java
import java.util.*;
class Event {
String type;
String entityId;
Map<String, Object> payload;
String timestamp;
Event(String type, String entityId, Map<String, Object> payload) {
this.type = type;
this.entityId = entityId;
this.payload = payload;
this.timestamp = new Date().toString();
}
}
class EventStore {
private final Map<String, List<Event>> streams = new HashMap<>();
void append(String entityId, Event event) {
streams.computeIfAbsent(entityId, k -> new ArrayList<>()).add(event);
}
List<Event> getStream(String entityId) {
return new ArrayList<>(streams.getOrDefault(entityId, List.of()));
}
}
class BankAccount {
String accountId;
double balance = 0;
int version = 0;
BankAccount(String accountId) {
this.accountId = accountId;
}
void apply(Event event) {
switch (event.type) {
case "Deposited" -> balance += (double) event.payload.get("amount");
case "Withdrawn" -> balance -= (double) event.payload.get("amount");
}
version++;
}
static BankAccount rehydrate(String accountId, List<Event> events) {
BankAccount account = new BankAccount(accountId);
events.forEach(account::apply);
return account;
}
}
// Usage
EventStore store = new EventStore();
String accountId = "ACC-123";
store.append(accountId, new Event("Deposited", accountId, Map.of("amount", 100.0)));
store.append(accountId, new Event("Withdrawn", accountId, Map.of("amount", 30.0)));
store.append(accountId, new Event("Deposited", accountId, Map.of("amount", 50.0)));
BankAccount account = BankAccount.rehydrate(accountId, store.getStream(accountId));
System.out.println("Balance: " + account.balance); // 120.0
Explanation
Event Sourcing replaces the traditional CRUD model with an append-only event log:
- Event Store: append-only log of all domain events per entity
- Events: immutable records describing what happened (e.g.,
MoneyDeposited,OrderShipped) - Entity Rehydration: rebuilding current state by replaying all events for an entity
- Projections: read-optimized views built by subscribing to the event stream
- Snapshots: the computed state saved periodically so you don’t replay thousands of events
The trade-offs matter as much as the mechanics:
- Replay cost grows with stream length. Rehydrating an entity is O(number of events), so long-lived entities need snapshots or reads become your latency bottleneck.
- Schema changes are forever. Renaming a field breaks replay of old events unless you add an upcaster or keep the legacy branch in
apply. Version event types from day one. - Reads are eventually consistent. A projection can lag behind the write side; a read right after a write may return stale data unless you track versions or wait on the consumer.
- The log only grows. Storage cost, retention policy, and GDPR erasure are design decisions, not afterthoughts.
Variants
| Variant | Description | Use Case |
|---|---|---|
| Full Event Sourcing | All state from events; no mutable DB | Maximum auditability; compliance |
| Hybrid | Events + current state snapshot | Performance; reduce replay cost |
| Temporal Queries | Query state at any point in time | Debugging; historical reporting |
| Event Replay | Replay events to rebuild or migrate | Schema migrations; bug recovery |
What Works
- Events should describe business intent (e.g.,
OrderPlaced) not technical actions (RowInserted) - Never delete or mutate events — the log is immutable
- Use snapshots for long-lived entities to avoid replaying thousands of events
- Version your event schemas for forward/backward compatibility
- Make consumers idempotent — the same event must be safe to process more than once
- Encrypt sensitive payload fields at the application level
Common Mistakes
- Using events as a message bus instead of a state store (separate concerns)
- Mutating or deleting events, breaking the audit trail
- Forgetting to handle event schema evolution (breaking old replays)
- Replaying all events from the beginning of time without snapshots
- Storing large binary payloads inside events instead of references
- Not handling duplicate event delivery in distributed systems
Production Notes
- Roll out event schema changes in two steps: deploy code that reads both the old and new version first, then start emitting the new version. Never break replay of historical streams.
- Alert on projection lag, not only on errors: a consumer falling behind the write side looks like data loss to users.
- Set a snapshot cadence per entity type (e.g., every 50–100 events) and measure rehydration time in staging with production-like stream lengths.
- Budget for storage growth: event stores only grow. Decide retention, archival, and payload size limits up front.
- Encrypt personal data per subject: crypto-shredding (deleting the subject’s key) is the practical answer to GDPR erasure when history can’t be rewritten.
Troubleshooting
- Replay produces wrong state: an old event schema no longer matches the current
applylogic. Add an upcaster for that version or keep the legacy branch inapply. - Read model returns stale data: the projection consumer fell behind the event store. Check consumer lag and have writes return the applied version so the UI can wait or refetch.
- Duplicate events in the stream: the producer retried after a store-side timeout. Make appends idempotent with a unique event ID or an expected-version check on write.
- Rehydration slows down over time: stream length outgrew the snapshot cadence. Shorten the snapshot interval or page through the stream.
- Personal data landed in event payloads: events can’t be deleted. Rotate the data-subject encryption key or append a
DataForgottenmarker event and honor it in projections.
Key Takeaways
- State is a fold over events: don’t store “the row”, store what happened and derive the rest.
- The event log is immutable and versioned — schema evolution is a day-one design constraint.
- Add snapshots when replay cost hurts, not by default.
- Event Sourcing isn’t a read model; pair it with CQRS when read and write workloads diverge.
Further Reading
Frequently Asked Questions
How do I handle schema changes in events?
Version your event types (OrderPlaced_v1, OrderPlaced_v2) and keep an upcaster that transforms old events into the current schema during replay.
Can I delete data under GDPR with Event Sourcing?
You can't delete events, but you can encrypt personal fields with a per-subject key and delete that key — crypto-shredding. Alternatively, append a DataForgotten event and honor it in projections.
How do snapshots work?
After every N events, save the computed entity state. On rehydration, load the latest snapshot and replay only the events after it, which keeps replay time roughly constant.
Is Event Sourcing the same as event-driven architecture?
No. Event-driven systems publish events to notify other services; Event Sourcing uses events as the source of truth for state. They combine well but either can exist alone.
Does Event Sourcing require CQRS?
Not strictly, but they pair naturally: the event store is the write side and projections build the read side. Without CQRS you still need a way to serve queries without replaying on every request.
How do I stop concurrent writers from corrupting a stream?
Use optimistic concurrency: append with an expected version and let the store reject the write if the stream moved on. The caller reloads the stream and retries.
Related Resources
CQRS Pattern
Separate read and write models to optimize each for its own workload — when CQRS pays off, when it's overkill, and how it pairs with event sourcing.
PatternSaga Pattern: Orchestration vs Choreography
Manage distributed transactions across multiple services by chaining local transactions with compensating actions for rollbacks. A microservices pattern.
PatternEvent-Carried State Transfer Pattern
Replicate state changes across services by publishing events that carry the full updated entity state, enabling consumers to maintain their own local copies without querying the source.
PatternIdempotent Consumer Pattern
Process messages from a queue exactly once regardless of duplicates by using idempotent operations, unique identifiers, and deduplication strategies at the consumer level.
RecipeImplement Event Sourcing in a Relational Database
Build event sourcing in a relational database. Store immutable events, project read models, and use snapshots with PostgreSQL, MySQL, and SQL Server.
GuideEvent Sourcing and CQRS: Event Store, Projections
Master event sourcing and CQRS: event store design, aggregate roots, projections, read models, snapshots, sagas, and production patterns for event-driven systems.