Why Event Sourcing
Store what happened, derive what is
The Problem
Traditional databases store current state. When you update a record, the previous state is lost. This creates fundamental problems for business systems:
You can't answer "what happened?"
An account balance is $1,000. How did it get there? What transactions occurred? Were any reversed? Traditional CRUD databases only know current values, not history.
Audit requirements demand history. Debugging requires understanding sequences of events. Business intelligence needs to analyze patterns over time. CRUD gives you a snapshot; you need a movie.
We needed a data model that:
- Preserves complete history by design
- Enables point-in-time reconstruction
- Supports audit and compliance requirements
- Allows evolving interpretations of the same data
Current Options
| Option | Pros | Cons |
|---|---|---|
| CRUD with Audit TablesTraditional database with separate audit logging. |
|
|
| Event SourcingStore events as source of truth; derive state. |
|
|
| Change Data CaptureCapture changes from traditional database as events. |
|
|
Future Outlook
Event sourcing is becoming standard for business-critical systems.
The audit requirement is universal now.
Regulations require knowing who did what when. Business intelligence requires historical analysis. AI training requires event sequences. The need for history is everywhere.
Event sourcing isn't just about compliance—it's about capability. When you have the full history, you can build features that CRUD can't support: undo, time travel debugging, what-if analysis, pattern detection.
The tooling is maturing. Event stores like EventStoreDB, Kafka with proper event schemas, and cloud-native solutions make implementation easier. The pattern is moving from "advanced technique" to "standard practice."
Our Decision
✓Why we chose this
- Complete audit trailEvery change is preserved; nothing is lost
- Point-in-time queriesReconstruct state at any historical moment
- Debugging superpowersReplay events to understand how state evolved
- Business flexibilityReinterpret historical events with new business logic
×Trade-offs we accept
- Learning curveDifferent mental model than CRUD
- Event versioningSchema changes require migration strategies
- Eventual consistencyProjections may lag behind events
Motivation
For a system that handles real business decisions—obligations, transactions, approvals—"trust us, the current state is correct" isn't acceptable.
Event sourcing gives us an immutable record of everything that happened. We can prove the sequence of events that led to any state. We can replay history to debug issues. We can rebuild projections when business logic changes.
This isn't overhead—it's the foundation of a trustworthy system. When a customer questions a transaction, we don't guess; we show them the exact sequence of events.
Recommendation
Start with event sourcing for new aggregates, especially where:
- Audit requirements exist
- Business logic changes frequently
- Multiple views of the same data are needed
Use a proven event store (EventStoreDB, Kafka) rather than building on a general database. The concurrency and ordering guarantees matter.
Design events around business intent, not data changes:
- Good:
InvoiceSent,PaymentReceived,InvoiceDisputed - Bad:
InvoiceStatusUpdated,BalanceChanged
Plan for event versioning from day one. Events are immutable; schemas must evolve gracefully.
Examples
// Domain events capture business intent
type InvoiceEvent =
| { type: 'InvoiceCreated'; invoiceId: string; customerId: string; amount: number; dueDate: string }
| { type: 'InvoiceSent'; invoiceId: string; sentAt: string; sentTo: string }
| { type: 'PaymentReceived'; invoiceId: string; amount: number; receivedAt: string }
| { type: 'InvoiceDisputed'; invoiceId: string; reason: string; disputedAt: string }
| { type: 'DisputeResolved'; invoiceId: string; resolution: 'refunded' | 'upheld'; resolvedAt: string };
// Reconstruct state by folding events
function reconstructInvoice(events: InvoiceEvent[]): Invoice {
return events.reduce((state, event) => {
switch (event.type) {
case 'InvoiceCreated':
return { ...state, id: event.invoiceId, amount: event.amount, status: 'draft' };
case 'InvoiceSent':
return { ...state, status: 'sent', sentAt: event.sentAt };
case 'PaymentReceived':
return { ...state, status: 'paid', paidAmount: (state.paidAmount ?? 0) + event.amount };
case 'InvoiceDisputed':
return { ...state, status: 'disputed', disputeReason: event.reason };
case 'DisputeResolved':
return { ...state, status: event.resolution === 'refunded' ? 'refunded' : 'paid' };
default:
return state;
}
}, {} as Invoice);
}Events describe what happened in business terms. State is derived by replaying events. The event log is the source of truth.