Event Sourcing Pattern
Introduction
Traditional systems persist current state in a database:
- Orders table has the latest order record.
- Bank accounts store the latest balance.
But this approach loses valuable history. For example:
- How did the account balance change over time?
- Which events led to the current state?
The Event Sourcing Pattern solves this by storing all changes (events) to application state as an append-only log.
- Current state is reconstructed by replaying events.
- Provides complete audit trails, replayability, and integration opportunities.
Intent
The Event Sourcing Pattern’s intent is to persist application state as a sequence of events, not just the latest snapshot, enabling auditability, traceability, and replay.
Structure
Core Components
Event Store
- Append-only log of domain events.
Event Publisher
- Persists and publishes new events.
Event Handlers / Projectors
- Consume events to update read models.
Read Models (Projections)
- Query-optimized views derived from events.
graph TD
A[Command] --> B[Event]
B --> C[Event Store]
C --> D[Read Model Projector]
D --> E[(Read Model)]
✅ Source of truth = events.
✅ Read models are projections.
Participants
Commands
- User intent that triggers domain behavior.
Domain Events
- Immutable records of what happened.
- Example:
OrderPlaced
,PaymentCompleted
.
Event Store
- Stores events in order, append-only.
Projections
- Build query-optimized models.
Collaboration Flow
- Command executes business logic.
- Emits domain event.
- Event persisted in Event Store.
- Handlers project event into read models.
Implementation in Java
Domain Event
public class OrderPlacedEvent {
private final String orderId;
private final double total;
private final Instant timestamp;
public OrderPlacedEvent(String orderId, double total) {
this.orderId = orderId; this.total = total;
this.timestamp = Instant.now();
}
public String getOrderId() { return orderId; }
public double getTotal() { return total; }
public Instant getTimestamp() { return timestamp; }
}
Event Store
public interface EventStore {
void save(Object event);
List<Object> getEvents(String aggregateId);
}
Event-Sourced Repository
public class OrderRepository implements EventStore {
private final Map<String, List<Object>> store = new HashMap<>();
@Override
public void save(Object event) {
store.computeIfAbsent("orders", k -> new ArrayList<>()).add(event);
}
@Override
public List<Object> getEvents(String aggregateId) {
return store.getOrDefault(aggregateId, new ArrayList<>());
}
}
Projection (Read Model)
@Service
public class OrderProjection {
private final Map<String, Double> orderTotals = new HashMap<>();
public void on(OrderPlacedEvent event) {
orderTotals.put(event.getOrderId(), event.getTotal());
}
public Double getTotal(String orderId) {
return orderTotals.get(orderId);
}
}
✅ Events stored, not just final state.
✅ Read model derived from replay.
Consequences
Benefits
- Auditability – Complete history of changes.
- Replayability – Rebuild state anytime.
- Integration – Events published to other systems.
- Debugging – Understand exact sequence of actions.
- Event-Driven Friendly – Naturally integrates with CQRS.
Drawbacks
- Complexity – More moving parts.
- Storage Growth – Event store grows indefinitely.
- Event Versioning – Schema evolution is tricky.
- Consistency – Must rebuild projections carefully.
Real-World Case Studies
1. Banking Systems
- Transactions stored as events.
- Balance derived by summing transactions.
2. E-commerce Platforms
- Orders tracked as events (
OrderPlaced
,OrderShipped
). - Enables customer history and analytics.
3. Event Stores (Tools)
- Axon Framework, EventStoreDB, Kafka.
- Provide ready-made support for event sourcing.
Extended Java Case Study
Traditional Approach (CRUD)
// Latest state only
public class BankAccount {
private String id;
private double balance;
public void deposit(double amount) { balance += amount; }
public void withdraw(double amount) { balance -= amount; }
}
❌ Loses history of deposits/withdrawals.
Event Sourcing Approach
public class BankAccount {
private String id;
private double balance;
private List<Object> changes = new ArrayList<>();
public void deposit(double amount) {
applyChange(new MoneyDepositedEvent(id, amount));
}
public void withdraw(double amount) {
applyChange(new MoneyWithdrawnEvent(id, amount));
}
private void applyChange(Object event) {
if(event instanceof MoneyDepositedEvent e) balance += e.getAmount();
if(event instanceof MoneyWithdrawnEvent e) balance -= e.getAmount();
changes.add(event);
}
}
✅ Full history retained.
✅ Balance reconstructed from events.
Interview Prep
Q1: What is the Event Sourcing Pattern?
Answer: A pattern where application state is persisted as a sequence of events, not just final state, enabling auditability and replay.
Q2: What are pros and cons of event sourcing?
Answer: Pros: auditability, replayability, integration. Cons: complexity, storage growth, event versioning.
Q3: How does event sourcing relate to CQRS?
Answer: Commands produce events (write side). Queries read projections built from events (read side).
Q4: When should you use event sourcing?
Answer: In systems needing auditability, history, or replay (banking, trading, e-commerce).
Q5: What challenges exist?
Answer: Event schema evolution, storage growth, rebuilding projections.
Visualizing Event Sourcing Pattern
graph TD
Cmd[Command] --> EV[Event]
EV --> Store[Event Store]
Store --> Proj[Projection]
Proj --> RM[(Read Model)]
✅ Events as source of truth.
✅ Read models derived from projections.
Key Takeaways
- Event Sourcing stores events, not just final state.
- Benefits: auditability, replayability, integration.
- Challenges: complexity, storage, event versioning.
- Works well with CQRS and event-driven systems.
Next Lesson
Next, we’ll cover Saga Pattern — managing long-running distributed transactions in microservices.