Problem Statement
Event Sourcing is a data storage pattern where you save every state-changing event that happens in your system, instead of saving just the current state. You might encounter this need when your traditional CRUD database starts feeling like a bottleneck—maybe you’re losing audit trails, struggling to debug complex workflows, or needing to replay past states for machine learning or compliance. Sound familiar? It’s the kind of problem that creeps up once your app grows past a simple to-do list.
Core Explanation
Event Sourcing flips your mental model upside down. Instead of updating a row in a database (like changing a user’s email from “old@example.com” to “new@example.com”), you append an event to an immutable log: “User changed their email from X to Y.” The current state of that user is then derived by replaying all events that have ever happened for them.
Here’s how it works in practice:
-
Events are facts. Each event represents something that happened in the past—never deleted, never updated. Example:
OrderPlaced,PaymentReceived,ItemShipped. - Event store is the source of truth. This is just a database optimized for appending events (often with a sequence ID or timestamp). You don’t have a separate “current state” table.
-
Projections build read-models. To answer “What is the user’s current email?” you run a function that folds all
EmailChangedevents for that user into a single value. This can be done on the fly or precomputed as a “projection” stored in a cache or separate read database. - Snapshots avoid endless replays. As events accumulate, you periodically take a snapshot of the state at a point in time. Later replays start from the snapshot instead of event #1.
Analogy: Think of a bank account ledger. Traditional CRUD is like keeping a sticky note with your current balance—you erase and rewrite it each time. Event Sourcing is keeping the full log of every deposit and withdrawal. To know your balance, you sum the log. Want to know what your balance was last Tuesday? Just replay the log up to that date. You never lose history.
Key components:
-
Event – immutable, named, timestamped data (e.g.,
{"type": "ItemAddedToCart", "timestamp": ..., "data": {"itemId":..., "quantity":1}}) - Event Store – append-only storage (like Kafka, EventStoreDB, or a dedicated table in PostgreSQL)
- Aggregate – a cluster of events that belong together (e.g., all events for one shopping cart)
- Projection – a function that transforms the event stream into a useful read model
Practical Context
When to use Event Sourcing:
- You need a full audit trail (financial systems, compliance-heavy apps)
- You want to debug or replay past system states (temporal queries, testing, or customer support)
- Your business logic is complex and involves long-running workflows that need to recover gracefully from failures
- You’re building microservices and need to share facts between services (event-driven architecture)
When NOT to use it:
- Your data is simple CRUD with low volume (a blog’s comment count or a user profile—overkill)
- You need strong eventual consistency guarantees and can’t accept temporary stale reads
- Your team is new to event-driven patterns and the operational complexity (event versioning, schema evolution, projection rebuilding) will slow you down
Common real-world use cases:
- E-commerce order management: Tracking every state change of an order (placed, paid, shipped, refunded) so you can later analyze funnel conversion, handle refunds, or reprocess failed payments.
- Banking & accounting: Immutable transaction logs for compliance, fraud detection, and year-end reporting.
- Collaborative tools (like GitHub): Recording every edit, comment, and merge as events so you can rebuild any historical version of a document or codebase.
Quick Example
Imagine a shopping cart. With traditional CRUD, you’d have a cart table with columns like user_id, items (a JSON blob), and total. On “add item,” you update the row. If something crashes mid-update, you might lose data.
With Event Sourcing, you store events:
// Appending an event to the event store
{
"eventType": "ItemAddedToCart",
"aggregateId": "cart-123",
"data": {
"itemId": "item-456",
"quantity": 2,
"price": 9.99
},
"timestamp": "2024-03-15T10:00:00Z"
}
To get the current cart state, you replay all events for cart-123 and fold them:
// Replay events to compute current cart
events.forEach(event => {
if (event.type === "ItemAddedToCart") {
cart.items.push(event.data);
cart.total += event.data.price * event.data.quantity;
}
// other event types...
});
This tiny example shows the core trade-off: you get perfect history and traceability, but you have to explicitly rebuild state (or maintain projections). It’s not more code—it’s different code.
Key Takeaway
Event Sourcing trades simplicity of state management for the superpower of immutable, replayable history. Use it when you care deeply about what happened, not just what is. For a deeper dive, start with Martin Fowler’s classic bliki post on Event Sourcing—it’s the gold standard for understanding the pattern’s nuances.
Top comments (0)