Definition
- Event Sourcing – A persistence pattern where the source of truth is an append-only, ordered log of immutable domain events rather than the current state.
- Rehydration – Loading all events for one aggregate and folding them one by one into an in-memory object to reconstruct current state.
- Snapshot – A periodically saved copy of the rehydrated state so you only replay events after the snapshot, not the full history.
- Projection – A separate read model built by consuming the event stream, optimized for specific query patterns.
- Event upcasting – Transforming old event versions into the current schema during replay, since events are immutable forever.
Think of Rajesh's bank account at National Bank of India. A normal system stores one number: balance is 950000 rupees. Event Sourcing instead stores the history like a passbook: opened with 1000000, then transferred 50000 to Priya, then received 20000 salary interest. To know the balance today, you start at zero and add up every line. The passbook never gets erased; you only ever add new lines to the bottom.
Understanding the Concepts
What is Event Sourcing?
In a traditional system you do an UPDATE on a row and the old value is gone forever. Event Sourcing forbids destructive updates. Every state change is captured as a domain event appended to an event store.
package com.bank.eventsourcing;
import java.math.BigDecimal;
import java.time.Instant;
public sealed interface AccountEvent {
String accountId();
Instant occurredAt();
record AccountOpened(String accountId, BigDecimal initialDeposit,
Instant occurredAt) implements AccountEvent {}
record MoneyDeposited(String accountId, BigDecimal amount,
String description, Instant occurredAt) implements AccountEvent {}
record MoneyWithdrawn(String accountId, BigDecimal amount,
String description, Instant occurredAt) implements AccountEvent {}
}
Event store for account SAV2001:
1. AccountOpened(SAV2001, 1000000.00, 2024-01-01)
2. MoneyWithdrawn(SAV2001, 50000.00, "Transfer to Priya", 2024-01-05)
3. MoneyDeposited(SAV2001, 20000.00, "Salary interest", 2024-01-15)
How do you reconstruct current state by replaying events?
You load all events for one aggregate and fold them one by one into an in-memory object. This folding is called rehydration.
package com.bank.eventsourcing;
import java.math.BigDecimal;
import java.util.List;
public class Account {
private String accountId;
private BigDecimal balance = BigDecimal.ZERO;
private int version = 0;
public static Account rehydrate(List<AccountEvent> events) {
Account account = new Account();
for (AccountEvent event : events) {
account.apply(event);
}
return account;
}
private void apply(AccountEvent event) {
switch (event) {
case AccountEvent.AccountOpened e -> {
this.accountId = e.accountId();
this.balance = e.initialDeposit();
}
case AccountEvent.MoneyDeposited e ->
this.balance = this.balance.add(e.amount());
case AccountEvent.MoneyWithdrawn e ->
this.balance = this.balance.subtract(e.amount());
}
this.version++;
}
public static void main(String[] args) {
var events = List.of(
new AccountEvent.AccountOpened("SAV2001",
new BigDecimal("1000000.00"), java.time.Instant.now()),
new AccountEvent.MoneyWithdrawn("SAV2001",
new BigDecimal("50000.00"), "Transfer to Priya", java.time.Instant.now()),
new AccountEvent.MoneyDeposited("SAV2001",
new BigDecimal("20000.00"), "Salary interest", java.time.Instant.now())
);
Account account = Account.rehydrate(events);
System.out.println("Account: " + account.accountId);
System.out.println("Balance: " + account.balance);
System.out.println("Version: " + account.version);
}
// Getters
public String getAccountId() { return accountId; }
public BigDecimal getBalance() { return balance; }
public int getVersion() { return version; }
}
Expected output:
Account: SAV2001
Balance: 970000.00
Version: 3
How do snapshots avoid replaying the full history?
Replaying thousands of events on every read is slow. A snapshot saves the rehydrated state at a known version, and you only replay events after it.
package com.bank.eventsourcing;
import java.math.BigDecimal;
public record AccountSnapshot(
String accountId,
BigDecimal balance,
int version
) {
public static AccountSnapshot of(Account account) {
return new AccountSnapshot(
account.getAccountId(),
account.getBalance(),
account.getVersion()
);
}
public static void main(String[] args) {
// After 500 events, take a snapshot
System.out.println("Snapshot at version 500: balance = 820000.00");
System.out.println("On next load: load snapshot + replay events 501-510");
System.out.println("Instead of replaying all 510 events from scratch");
}
}
Expected output:
Snapshot at version 500: balance = 820000.00
On next load: load snapshot + replay events 501-510
Instead of replaying all 510 events from scratch
What are the advantages of Event Sourcing?
The advantages are compelling for banking: a complete audit trail, temporal queries, retroactive bug fixes, and the freedom to build new read models later.
package com.bank.eventsourcing;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
public class TemporalQuery {
public static BigDecimal balanceAsOf(List<AccountEvent> events, Instant cutoff) {
Account account = new Account();
for (AccountEvent event : events) {
if (event.occurredAt().isAfter(cutoff)) break;
// account.apply(event); // Apply only events up to cutoff
}
return account.getBalance();
}
public static void main(String[] args) {
System.out.println("Advantages of Event Sourcing:");
System.out.println(" 1. Audit trail: every change is recorded with timestamp and reason");
System.out.println(" 2. Temporal queries: 'What was Rajesh balance on Jan 3rd?'");
System.out.println(" 3. Bug fix replay: fix the logic, replay events, get correct state");
System.out.println(" 4. New read models: replay events into any new projection shape");
System.out.println(" 5. Debugging: replay the exact sequence that led to a bug");
}
}
Expected output:
Advantages of Event Sourcing:
1. Audit trail: every change is recorded with timestamp and reason
2. Temporal queries: 'What was Rajesh balance on Jan 3rd?'
3. Bug fix replay: fix the logic, replay events, get correct state
4. New read models: replay events into any new projection shape
5. Debugging: replay the exact sequence that led to a bug
What are the disadvantages?
The model is more complex than CRUD, querying requires separate projections, and event schema evolution is hard because events are immutable forever.
package com.bank.eventsourcing;
public class EventSourcingDisadvantages {
public static void main(String[] args) {
System.out.println("Disadvantages:");
System.out.println(" 1. Complexity: harder to build and reason about than CRUD");
System.out.println(" 2. Querying: 'all accounts > 100K' requires a separate projection");
System.out.println(" 3. Schema evolution: MoneyWithdrawn v1 vs v2 needs upcasting");
System.out.println(" 4. Eventual consistency: projections lag behind the event log");
System.out.println(" 5. Storage: event log grows unbounded without compaction");
}
}
Expected output:
Disadvantages:
1. Complexity: harder to build and reason about than CRUD
2. Querying: 'all accounts > 100K' requires a separate projection
3. Schema evolution: MoneyWithdrawn v1 vs v2 needs upcasting
4. Eventual consistency: projections lag behind the event log
5. Storage: event log grows unbounded without compaction
How is Event Sourcing different from CQRS?
Event Sourcing is about how you persist (append-only events). CQRS is about separating the write model from the read model. They are frequently paired but are not the same thing.
| Aspect | Event Sourcing | CQRS |
|---|---|---|
| Focus | Persistence mechanism | Architectural separation |
| Core idea | Store events, derive state | Separate write model from read model |
| Can be used alone | Yes, without CQRS | Yes, without Event Sourcing |
| Common pairing | Event log naturally feeds CQRS projections | Read projections naturally consume event streams |
package com.bank.eventsourcing;
public class EventSourcingVsCqrs {
public static void main(String[] args) {
System.out.println("Event Sourcing WITHOUT CQRS:");
System.out.println(" Store events, rebuild state on read from event log");
System.out.println(" Same model for reads and writes");
System.out.println();
System.out.println("CQRS WITHOUT Event Sourcing:");
System.out.println(" Normal JPA entities for writes");
System.out.println(" Separate denormalized table for reads");
System.out.println(" Sync via @TransactionalEventListener");
System.out.println();
System.out.println("Event Sourcing WITH CQRS (common):");
System.out.println(" Event log is the write store");
System.out.println(" Projections are the read store");
}
}
Expected output:
Event Sourcing WITHOUT CQRS:
Store events, rebuild state on read from event log
Same model for reads and writes
CQRS WITHOUT Event Sourcing:
Normal JPA entities for writes
Separate denormalized table for reads
Sync via @TransactionalEventListener
Event Sourcing WITH CQRS (common):
Event log is the write store
Projections are the read store
What about event schema evolution?
Once an event is written it is immutable forever. If the shape of MoneyWithdrawn changes, you must support old versions with upcasting.
package com.bank.eventsourcing;
import java.math.BigDecimal;
public class EventUpcaster {
// V1 event stored amount as double (old, incorrect)
// V2 event stores amount as BigDecimal string (correct)
public static AccountEvent.MoneyWithdrawn upcast(String rawJson) {
// Parse the raw JSON and check the version
// If version 1: convert double to BigDecimal
// If version 2: use BigDecimal directly
System.out.println("Upcasting v1 event: amount 50000.0 -> BigDecimal 50000.00");
return new AccountEvent.MoneyWithdrawn(
"SAV2001",
new BigDecimal("50000.00"),
"Transfer to Priya",
java.time.Instant.now()
);
}
public static void main(String[] args) {
upcast("{\"amount\": 50000.0, \"version\": 1}");
}
}
Expected output:
Upcasting v1 event: amount 50000.0 -> BigDecimal 50000.00
What should a senior engineer know?
Use a purpose-built event store like EventStoreDB or an append-only Kafka log with snapshots. The apply method is used both for new events and for replay, guaranteeing consistency. Monitor event store size, snapshot frequency, and projection lag.
Production checklist:
[x] Events are immutable, append-only
[x] Snapshots taken every 100-500 events
[x] Projections rebuild from events for new read models
[x] Schema registry for event version compatibility
[x] Upcasters for backward compatibility
[x] Event store: EventStoreDB or Kafka with compaction
[x] Monitor: projection lag, event store size, replay time
Visual Overview
Event Sourcing Flow:
Command: "Withdraw 50000 from SAV2001"
│
▼
┌──────────────┐ ┌──────────────────────────────┐
│ Account │ │ Event Store │
│ Aggregate │────▶│ 1. AccountOpened(1000000) │
│ │ │ 2. MoneyWithdrawn(50000) │
│ validate() │ │ 3. MoneyDeposited(20000) │
│ apply(event) │ │ ... append-only, immutable │
└──────────────┘ └──────────────┬───────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Projection │ │ Snapshot │
│ (read model)│ │ (version N) │
│ balance view│ │ balance: │
│ for queries │ │ 970000.00 │
└──────────────┘ └──────────────┘
Key Takeaway
Event Sourcing is a persistence pattern where the source of truth is an append-only, ordered log of immutable domain events rather than the current state. Instead of storing a row saying the balance is 970000, you store every event: opened with 1000000, withdrew 50000 for Priya's transfer, deposited 20000 from salary interest. The current balance is derived by replaying those events.
The advantages are compelling for banking. You get a complete, tamper-evident audit trail for free, which regulators at National Bank of India demand. You can answer temporal queries like what Rajesh's balance was at midnight on month-end by replaying only up to that timestamp. You can fix bugs and replay corrected logic, and you can build entirely new read models later by replaying the same events into a new projection. The disadvantages are equally real: it is more complex than CRUD, querying requires separate projections, and event schema evolution is hard because events are immutable forever.
At National Bank of India, auditors once could not explain why Rajesh's balance dropped by 50000 on a given day because the account table had overwritten the old value. Pankaj moved that ledger to Event Sourcing so every change became an appended event, and after that they could reconstruct any historical balance. The team pairs Event Sourcing with CQRS so the append-only write log feeds denormalized read projections, and in production they use EventStoreDB with periodic snapshots.
Remember one sentence: Event Sourcing persists every state change as an immutable, ordered event and derives current state by replaying the log, trading extra complexity for a perfect audit trail, time travel, and the freedom to build new read models from the same history.
---