Event Sourcing Basics
What if instead of storing the current state of your bank account ($1,000), you stored every transaction that ever happened? Deposit $500, withdrawal $200, deposit $700...
You could always derive the current balance by replaying the transactions. But more importantly, you'd have a complete audit trail. That's Event Sourcing.
State-Based vs Event-Based
TRADITIONAL: Store Current State
Account: #12345
Balance: $1,000
Updated: 2024-01-15
(What happened? No idea.)
EVENT SOURCING: Store What Happened
AccountOpened { amount: $0 }
MoneyDeposited { amount: $500 }
MoneyWithdrawn { amount: $200 }
MoneyDeposited { amount: $700 }
Current: $0 + $500 - $200 + $700 = $1,000
Events Are Immutable Facts
Events describe something that happened in the past. They are:
Cannot be changed or deleted
Sequence matters for replay
OrderPlaced, not PlaceOrder
All relevant data included
Modeling Domain Events
Events should capture business meaning, not technical changes:
// BAD: Technical, meaningless
interface RowUpdated {
table: string;
column: string;
oldValue: any;
newValue: any;
}
// GOOD: Business-meaningful events
interface OrderPlaced {
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number; price: number }>;
totalAmount: number;
placedAt: Date;
}
interface OrderShipped {
orderId: string;
trackingNumber: string;
carrier: string;
shippedAt: Date;
estimatedDelivery: Date;
}
interface OrderDelivered {
orderId: string;
deliveredAt: Date;
signedBy: string;
}
The Event Stream
An aggregate (like an Order) has an event stream - the complete history of everything that happened to it:
Order #ORD-123 Event Stream:
─────────────────────────────────────────────────
│ Seq │ Event │ Data │
─────────────────────────────────────────────────
│ 1 │ OrderPlaced │ {items: [...], ...} │
│ 2 │ PaymentReceived │ {amount: $99.99} │
│ 3 │ OrderShipped │ {tracking: "1Z999"} │
│ 4 │ OrderDelivered │ {signedBy: "John"} │
─────────────────────────────────────────────────
Current State = Apply(event1) → Apply(event2) → Apply(event3) → Apply(event4)
Rebuilding State from Events
interface OrderState {
id: string;
status: 'pending' | 'paid' | 'shipped' | 'delivered';
items: OrderItem[];
totalAmount: number;
trackingNumber?: string;
}
function rebuildOrder(events: OrderEvent[]): OrderState {
// Start with empty state
let state: OrderState = {
id: '',
status: 'pending',
items: [],
totalAmount: 0
};
// Apply each event in order
for (const event of events) {
state = applyEvent(state, event);
}
return state;
}
function applyEvent(state: OrderState, event: OrderEvent): OrderState {
switch (event.type) {
case 'OrderPlaced':
return {
...state,
id: event.orderId,
items: event.items,
totalAmount: event.totalAmount,
status: 'pending'
};
case 'PaymentReceived':
return { ...state, status: 'paid' };
case 'OrderShipped':
return {
...state,
status: 'shipped',
trackingNumber: event.trackingNumber
};
case 'OrderDelivered':
return { ...state, status: 'delivered' };
default:
return state;
}
}
Real-World: Accounting Systems
Accountants figured this out 500 years ago. In double-entry bookkeeping, you never modify a ledger entry - you add correcting entries. Every transaction is recorded, and the current balance is derived by summing all entries. Event Sourcing applies this same principle to software.
Why Event Sourcing?
| Benefit | Why It Matters |
|---|---|
| Complete Audit Trail | Every change is recorded. Perfect for compliance, debugging, analytics. |
| Time Travel | Rebuild state at any point in time. "What was the order at 3pm yesterday?" |
| Retroactive Bug Fixes | Fix a projection bug, replay events, correct all affected data. |
| Event-Driven Integration | Other services can subscribe to events. Natural fit for microservices. |
| Multiple Read Models | Build different views from the same events. Analytics, search, reporting. |
Real-World: Event Sourcing at Scale
Uses event sourcing for their billing system. Every subscription change, payment, and plan modification is an event. They can replay history to fix billing issues or generate accurate revenue reports.
Trip events (request, match, pickup, dropoff, payment) are stored as an event stream. Enables real-time tracking, billing accuracy, and dispute resolution by replaying exact trip history.
The Challenges
- Event Schema Evolution: How do you handle old events when the schema changes?
- Replay Performance: Rebuilding state from millions of events is slow (solved by snapshots)
- Event Ordering: Distributed systems make global ordering hard
- Learning Curve: Teams need to think differently about data
Commands vs Events
Don't confuse them:
COMMAND (Intent to do something) EVENT (Something that happened)
───────────────────────────────── ─────────────────────────────────
PlaceOrder → OrderPlaced
CancelOrder → OrderCancelled
ShipOrder → OrderShipped
Commands: Events:
- Imperative: "Do this!" - Past tense: "This happened"
- Can be rejected - Already happened, can't reject
- Represents intent - Represents fact
Key Takeaways
- Events are the source of truth - Current state is derived, not stored directly
- Events are immutable facts - Past tense, self-contained, never modified
- Rebuild state by replay - Apply events in order to get current state
- Complete audit trail - Every change is recorded forever
Next up: Projections - Building optimized read models from your event stream.