What is Event Sourcing?
Event Sourcing stores the state of a system as a sequence of immutable events rather than just the current state. Instead of UPDATE, you APPEND.
Key insight: The database stores "what happened" not "what is."
The Problem with CRUD
-- Traditional CRUD UPDATE accounts SET balance = 500 WHERE id = 1 Problem: You lost all history! - What was the old balance? - Who made the change? - When did it happen? - Why did it change?
Event Sourcing solution:
Don't store: balance = 500 Store instead: 1. AccountCreated(id=1, balance=0, timestamp=T1) 2. MoneyDeposited(id=1, amount=300, timestamp=T2) 3. MoneyDeposited(id=1, amount=200, timestamp=T3) Current balance = 0 + 300 + 200 = 500
How Event Sourcing Works
Event Store
Event Store (append-only log): ┌─────────────────────────────────────┐ │ Event 1: AccountCreated │ │ Event 2: MoneyDeposited(+100) │ │ Event 3: MoneyWithdrawn(-20) │ │ Event 4: MoneyDeposited(+50) │ │ ... │ └─────────────────────────────────────┘ Current State = Replay all events
State Reconstruction
graph LR
E1[Event 1<br/>Created] --> E2[Event 2<br/>+100]
E2 --> E3[Event 3<br/>-20]
E3 --> E4[Event 4<br/>+50]
E4 --> State[Current State<br/>Balance: 130]
Implementation
Event Store Schema
// Event definition
interface Event {
id: string;
aggregateId: string; // Entity this event belongs to
eventType: string;
data: any;
timestamp: Date;
version: number;
}
// Example events
class AccountCreated implements Event {
id: string;
aggregateId: string;
eventType = 'AccountCreated';
data: {
owner: string;
initialBalance: number;
};
timestamp: Date;
version: number;
}
class MoneyDeposited implements Event {
id: string;
aggregateId: string;
eventType = 'MoneyDeposited';
data: {
amount: number;
source: string;
};
timestamp: Date;
version: number;
}
Event Store Implementation
class EventStore {
private events: Event[] = [];
// Append event (never update!)
async append(event: Event): Promise<void> {
// Optimistic concurrency check
const latestVersion = await this.getLatestVersion(event.aggregateId);
if (event.version !== latestVersion + 1) {
throw new Error('Concurrency conflict');
}
this.events.push(event);
await this.persist(event);
await this.publish(event); // Notify subscribers
}
// Get all events for an aggregate
async getEvents(aggregateId: string): Promise<Event[]> {
return this.events.filter(e => e.aggregateId === aggregateId);
}
// Get events after specific version
async getEventsSince(aggregateId: string, version: number): Promise<Event[]> {
return this.events.filter(
e => e.aggregateId === aggregateId && e.version > version
);
}
private async persist(event: Event): Promise<void> {
// Store in database (PostgreSQL, EventStoreDB, etc.)
await db.query(
'INSERT INTO events (id, aggregate_id, event_type, data, timestamp, version) VALUES ($1, $2, $3, $4, $5, $6)',
[event.id, event.aggregateId, event.eventType, event.data, event.timestamp, event.version]
);
}
private async publish(event: Event): Promise<void> {
// Publish to message bus for CQRS projections
await messageBus.publish('events', event);
}
}
###Aggregate Root
class Account {
private id: string;
private balance: number = 0;
private version: number = 0;
private uncommittedEvents: Event[] = [];
// Reconstruct from events
static async load(id: string, eventStore: EventStore): Promise<Account> {
const account = new Account(id);
const events = await eventStore.getEvents(id);
for (const event of events) {
account.apply(event, false);
}
return account;
}
// Commands (business logic)
deposit(amount: number, source: string): void {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
const event = new MoneyDeposited({
id: uuid(),
aggregateId: this.id,
eventType: 'MoneyDeposited',
data: { amount, source },
timestamp: new Date(),
version: this.version + 1
});
this.apply(event, true);
}
withdraw(amount: number): void {
if (amount > this.balance) {
throw new Error('Insufficient funds');
}
const event = new MoneyWithdrawn({
id: uuid(),
aggregateId: this.id,
eventType: 'MoneyWithdrawn',
data: { amount },
timestamp: new Date(),
version: this.version + 1
});
this.apply(event, true);
}
// Apply event to state
private apply(event: Event, isNew: boolean): void {
switch (event.eventType) {
case 'AccountCreated':
this.balance = event.data.initialBalance;
break;
case 'MoneyDeposited':
this.balance += event.data.amount;
break;
case 'MoneyWithdrawn':
this.balance -= event.data.amount;
break;
}
this.version = event.version;
if (isNew) {
this.uncommittedEvents.push(event);
}
}
// Save to event store
async save(eventStore: EventStore): Promise<void> {
for (const event of this.uncommittedEvents) {
await eventStore.append(event);
}
this.uncommittedEvents = [];
}
}
Usage Example
// Create account
const account = new Account('acc-123');
account.create('John Doe', 0);
await account.save(eventStore);
// Deposit money
const loadedAccount = await Account.load('acc-123', eventStore);
loadedAccount.deposit(100, 'salary');
loadedAccount.deposit(50, 'bonus');
await loadedAccount.save(eventStore);
// Withdraw money
const acc2 = await Account.load('acc-123', eventStore);
acc2.withdraw(30);
await acc2.save(eventStore);
// Event store now contains:
// 1. AccountCreated(balance=0)
// 2. MoneyDeposited(amount=100, source='salary')
// 3. MoneyDeposited(amount=50, source='bonus')
// 4. MoneyWithdrawn(amount=30)
// Current balance = 0 + 100 + 50 - 30 = 120
CQRS (Command Query Responsibility Segregation)
Separate read and write models. Writes go to event store, reads go to optimized projections.
┌──────────┐ Command ┌─────────────┐
│ Client │────────2────────▶│Event Store │
└──────────┘ │(Write Model)│
│ └──────┬──────┘
│ │ Events
│ Query ▼
│ ┌─────────────┐
└────1────────▶ │ Projections │
│(Read Model) │
└─────────────┘
Why CQRS?
Problem with traditional CRUD:
-- Complex report query SELECT u.name, COUNT(o.id) as order_count, SUM(o.amount) as total_spent, AVG(o.amount) as avg_order FROM users u JOIN orders o ON u.id = o.user_id JOIN order_items oi ON o.id = oi.order_id WHERE u.created_at > '2024-01-01' GROUP BY u.id -- Slow on write-optimized tables!
CQRS solution:
// Projection (read model) - pre-computed
interface UserSummary {
userId: string;
name: string;
orderCount: number;
totalSpent: number;
avgOrder: number;
lastUpdated: Date;
}
// Simple, fast query
const summary = await db.userSummaries.findById(userId);
CQRS Implementation
// Write side
class OrderService {
async createOrder(userId: string, items: OrderItem[]): Promise<void> {
const order = new Order(userId);
order.create(items);
await order.save(eventStore);
// Events: OrderCreated, LineItemAdded (x N)
}
}
// Read side - Projection
class UserSummaryProjection {
async handle(event: Event): Promise<void> {
switch (event.eventType) {
case 'OrderCreated':
await this.updateUserSummary(event.data.userId, {
orderCount: +1
});
break;
case 'LineItemAdded':
await this.updateUserSummary(event.data.userId, {
totalSpent: +event.data.price
});
break;
}
}
private async updateUserSummary(userId: string, updates: Partial<UserSummary>): Promise<void> {
// Update read model
await db.query(
'UPDATE user_summaries SET order_count = order_count + $1, total_spent = total_spent + $2 WHERE user_id = $3',
[updates.orderCount || 0, updates.totalSpent || 0, userId]
);
}
}
// Event bus wires them together
eventBus.subscribe('OrderCreated', (event) => {
userSummaryProjection.handle(event);
});
Benefits of Event Sourcing
1. Perfect Audit Log
// Get full history
const events = await eventStore.getEvents('acc-123');
// [
// { eventType: 'AccountCreated', timestamp: '2024-01-01', ... },
// { eventType: 'MoneyDeposited', amount: 100, timestamp: '2024-01-05', ... },
// { eventType: 'MoneyWithdrawn', amount: 20, timestamp: '2024-01-10', ... }
// ]
// Who, what, when, why - all preserved!
2. Time Travel (Temporal Queries)
// "What was the account balance on January 5th?"
async function getBalanceAt(accountId: string, timestamp: Date): Promise<number> {
const events = await eventStore.getEvents(accountId);
const pastEvents = events.filter(e => e.timestamp <= timestamp);
let balance = 0;
for (const event of pastEvents) {
if (event.eventType === 'MoneyDeposited') {
balance += event.data.amount;
} else if (event.eventType === 'MoneyWithdrawn') {
balance -= event.data.amount;
}
}
return balance;
}
const balanceOnJan5 = await getBalanceAt('acc-123', new Date('2024-01-05'));
3. Event Replay & New Projections
// Business wants new report - "users by signup source"
// No problem! Replay all UserCreated events
class SignupSourceProjection {
async rebuild(): Promise<void> {
const events = await eventStore.getAllEvents('UserCreated');
for (const event of events) {
await db.query(
'INSERT INTO signup_sources (source, count) VALUES ($1, 1) ON CONFLICT (source) DO UPDATE SET count = count + 1',
[event.data.source]
);
}
}
}
4. Debugging
// Production bug: "User account balance is wrong" const events = await eventStore.getEvents(userId); console.log(events); // See EXACTLY what happened, in order // Event 47: MoneyWithdrawn(-500) ← Bug! Should have been rejected
Challenges & Solutions
1. Performance (Replaying Many Events)
Problem: Account with 1 million transactions → slow to replay
Solution: Snapshots
interface Snapshot {
aggregateId: string;
version: number;
state: any;
timestamp: Date;
}
class Account {
static async load(id: string, eventStore: EventStore): Promise<Account> {
// Load latest snapshot
const snapshot = await snapshotStore.getLatest(id);
const account = new Account(id);
if (snapshot) {
account.balance = snapshot.state.balance;
account.version = snapshot.version;
}
// Replay only events after snapshot
const events = await eventStore.getEventsSince(id, snapshot?.version || 0);
for (const event of events) {
account.apply(event, false);
}
return account;
}
// Create snapshot every 100 events
async saveWithSnapshot(eventStore: EventStore, snapshotStore: SnapshotStore): Promise<void> {
await this.save(eventStore);
if (this.version % 100 === 0) {
await snapshotStore.save({
aggregateId: this.id,
version: this.version,
state: { balance: this.balance },
timestamp: new Date()
});
}
}
}
2. Eventual Consistency
Problem: Write succeeds, but projection not updated yet → stale reads
Solution:
// Option 1: Return version to client
const result = await orderService.createOrder(userId, items);
// { orderId: '123', version: 5 }
// Client polls until version matches
while (true) {
const order = await orderQuery.getOrder(orderId);
if (order.version >= result.version) break;
await sleep(100);
}
// Option 2: Inline projection for critical paths
async function createOrder(userId: string, items: OrderItem[]): Promise<Order> {
const order = new Order(userId);
order.create(items);
await order.save(eventStore);
// Immediately update read model for this specific order
await orderProjection.handleSync(order.getUncommittedEvents());
return order;
}
3. Schema Evolution
Problem: Old events have different structure
Solution: Upcasting
class EventUpcast {
upcast(event: Event): Event {
if (event.eventType === 'MoneyDeposited' && event.version < 2) {
// Old version: { amount }
// New version: { amount, currency, source }
return {
...event,
data: {
amount: event.data.amount,
currency: 'USD', // Default
source: 'unknown' // Default
},
version: 2
};
}
return event;
}
}
Real-World Examples
Banking (Event Sourcing)
Events: - AccountOpened(accountId, owner, initialBalance) - MoneyDeposited(accountId, amount, source, timestamp) - MoneyWithdrawn(accountId, amount, atm_location, timestamp) - InterestCalculated(accountId, amount, rate, timestamp) - FeeCharged(accountId, amount, reason, timestamp) Benefits: - Perfect audit for regulators - Fraud detection (replay suspicious transactions) - Customer service (see exact transaction history)
E-commerce (CQRS)
Write Model: - Product catalog (normalized) - Order processing Read Models: - Product search (Elasticsearch) - User order history (denormalized) - Analytics dashboard (pre-aggregated) - Recommendation engine (ML-optimized format)
Stock Trading
Events (millions per second): - OrderPlaced(symbol, quantity, price, timestamp) - OrderMatched(buyOrderId, sellOrderId, price, quantity) - OrderCancelled(orderId, reason) Projections: - Order book (real-time bid/ask) - User portfolio (current holdings) - Trade history - Market data feed
Interview Tips 💡
When discussing Event Sourcing & CQRS in interviews:
- Explain the value: "For banking, we need perfect audit trail. Event Sourcing gives us immutable history..."
- Address performance: "For 1M events, use snapshots every 100 events to avoid full replay..."
- CQRS justification: "Read and write patterns differ. Orders need ACID writes, but product search needs fast full-text queries..."
- Eventual consistency: "CQRS projections are eventually consistent. For critical reads, update projection synchronously..."
- Real examples: "Banking uses Event Sourcing for compliance, e-commerce uses CQRS for search..."
Related Concepts
- Message Queues — Event delivery mechanism
- Database Replication — Synchronizing read models
- CAP Theorem — Eventual consistency trade-offs
- Microservices — CQRS in distributed systems
- Kafka — Event streaming platform
About ScaleWiki
ScaleWiki is an interactive educational platform dedicated to demystifying distributed systems, software architecture, and system design. Our mission is to provide high-quality, technically accurate resources for software engineers preparing for interviews or solving complex scaling challenges in production.
Read more about our Editorial Guidelines & Authorship.
Educational Disclaimer: The architectural patterns and system designs discussed in this article are based on common industry practices, technical whitepapers, and public engineering blogs. Actual implementations in enterprise environments may vary significantly based on specific product requirements, legacy constraints, and evolving technologies.
Related Articles
Caching Strategies
A breakdown of where to place your cache and how to keep it in sync with your database.
Microservices Architecture
An architectural style that structures an application as a collection of loosely coupled, independently deployable services.
Scaling: Overview
Comprehensive guide to scaling systems from 100 to 100 million users, covering vertical scaling (scale up), horizontal scaling (scale out), database sharding, caching strategies, and real-world architecture patterns from Netflix, Instagram, and Twitter.