Event Sourcing
AdvancedEvent sourcing stores the state of a system as a sequence of immutable events rather than mutable rows. Current state is derived by replaying events. It provides a complete audit trail and enables temporal queries.
Overview
In traditional systems, the database stores the current state — when you update a record, the previous value is lost. Event sourcing takes a different approach: instead of storing current state, it stores every state-changing event as an immutable fact in an event store (append-only log). The current state is derived by replaying all events from the beginning (or from a snapshot). For example, a bank account does not store "balance = $500"; it stores events: "AccountCreated", "Deposited($1000)", "Withdrawn($300)", "Deposited($200)", "Withdrawn($400)". Replaying these yields the current balance. Benefits include complete audit trail (every change is recorded), temporal queries (what was the state at any point in time?), debugging (replay events to reproduce bugs), and event-driven integration (events can be consumed by other services). The main challenges are event store size (snapshots needed), event schema evolution, and the learning curve.
Event Store vs Traditional DB
A traditional DB stores current state (mutable). An event store stores a sequence of immutable events. Current state is computed by replaying events from a starting point.
// Traditional DB — stores current state
// | account_id | balance |
// | A1 | 500.00 |
// Previous values are lost on UPDATE
// Event sourcing — stores all changes as events
// Event Store:
// | event_id | aggregate_id | type | data | timestamp |
// | 1 | A1 | Created | { owner: "Alice" }| 2025-01-01 |
// | 2 | A1 | Deposited | { amount: 1000 } | 2025-01-05 |
// | 3 | A1 | Withdrawn | { amount: 300 } | 2025-01-10 |
// | 4 | A1 | Deposited | { amount: 200 } | 2025-02-01 |
// | 5 | A1 | Withdrawn | { amount: 400 } | 2025-03-01 |
//
// Current balance = replay: 0 + 1000 - 300 + 200 - 400 = 500
// Balance on Jan 10? Replay events 1-3: 0 + 1000 - 300 = 700Implementation Pattern
Commands produce events. Events are appended to the event store. Projections (read models) are built by consuming events. Snapshots optimise replay for aggregates with many events.
// Event sourcing implementation
public class BankAccount {
private String id;
private BigDecimal balance = BigDecimal.ZERO;
private List<DomainEvent> uncommittedEvents = new ArrayList<>();
// Command → validates + produces event
public void deposit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0)
throw new IllegalArgumentException("Amount must be positive");
apply(new MoneyDeposited(id, amount, Instant.now()));
}
public void withdraw(BigDecimal amount) {
if (balance.compareTo(amount) < 0)
throw new InsufficientFundsException();
apply(new MoneyWithdrawn(id, amount, Instant.now()));
}
// Event handler — updates in-memory state
private void on(MoneyDeposited event) {
this.balance = balance.add(event.getAmount());
}
private void on(MoneyWithdrawn event) {
this.balance = balance.subtract(event.getAmount());
}
// Reconstitute from event history
public static BankAccount fromHistory(List<DomainEvent> events) {
BankAccount account = new BankAccount();
events.forEach(account::apply);
return account;
}
}Snapshots & Projections
Snapshots capture state at a point in time to avoid replaying all events. Projections build read-optimised views from events (often combined with CQRS).
// Snapshot — avoid replaying 10,000 events
// Every 100 events, save a snapshot of current state
// Load: snapshot + replay only events AFTER snapshot
//
// Event 1–100 → Snapshot at event 100: { balance: 5000 }
// Events 101–105 → replay 5 events from snapshot
// Much faster than replaying all 105 events
// Projection — build read model from events
@Component
public class AccountBalanceProjection {
@EventHandler
public void on(MoneyDeposited event) {
// Update read-optimised balance table
jdbcTemplate.update(
"UPDATE account_balances SET balance = balance + ? WHERE id = ?",
event.getAmount(), event.getAccountId());
}
@EventHandler
public void on(MoneyWithdrawn event) {
jdbcTemplate.update(
"UPDATE account_balances SET balance = balance - ? WHERE id = ?",
event.getAmount(), event.getAccountId());
}
}
// Read model is eventually consistent with event store
// But optimised for fast queries (no replay needed)Key Points to Remember
- 1Event sourcing stores all state changes as immutable events — current state is derived by replay.
- 2Provides complete audit trail, temporal queries, and natural integration with event-driven systems.
- 3Snapshots prevent performance degradation from replaying long event histories.
- 4Projections build read-optimised views from events — often combined with CQRS.
- 5Event schema evolution is the biggest long-term challenge — plan for versioning from day one.
Interview Questions
Sign in to ask AriaWhat is event sourcing and how does it differ from traditional state storage?
How do snapshots improve performance in event-sourced systems?
How does event sourcing relate to CQRS?
What are the challenges of event schema evolution?
Design an event-sourced banking system that supports temporal queries.
Ask Aria about Event Sourcing
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.