Event-Driven Architecture Patterns
Event Sourcing — Rebuilding State from Events
Storing state as an immutable log of events and playing them back to restore memory.
Introduction
Traditional relational databases only store the current state of a record, overwriting historical details during updates. Event Sourcing is a design pattern where all changes to application state are stored as an immutable sequence of domain events. The current state is built dynamically by replaying these events from the beginning.
Why It Matters
Event sourcing provides a 100% accurate, complete, and unalterable audit trail out of the box. It permits time-travel querying (recreating state as of any exact timestamp), simplifies debugging by reproducing historical state on local development machines, and eliminates Object-Relational Mapping (ORM) mismatch problems.
Real-World Analogy
Think of event sourcing like a bank ledger sheet. Your bank does not store your balance as a single cell that gets updated on a database row. Instead, the bank records every single deposit, withdrawal, and transfer. To determine your current balance, the ledger sum is calculated by replaying all transactions. The transactional ledger is the event source; your balance is the projection.
How It Works
State restoration and command execution in an Event-Sourced system operates as follows:
- Command Dispatch: A user submits a change command (e.g.,
DepositMoney) to the business domain. - Event Creation: The aggregate roots load historical events to restore their in-memory state, validate the command rules, and emit a state change event (e.g.,
MoneyDeposited). - Durable Append: The event is appended to the event store database. Once written, the event triggers downstream projectors.
Internal Architecture
As aggregates process more transactions over time, replaying thousands of events to rebuild state becomes slow. To optimize startup latency, systems implement Snapshotting. A snapshot represents the aggregate's state at a specific version (e.g., version 100). The application loads the latest snapshot first, and only replays events that occurred after the snapshot was saved.
Visual Explanation
Below is an ASCII representation showing how aggregate state is reconstructed using snapshots and event replay sequences:
[Snapshot: Ver 100] ──> Loads in-memory (Balance: $1000)
│
▼
[Event Store Log] ──> Replays subsequent events:
│ - Event 101: Deposited $50
│ - Event 102: Withdrew $20
▼
[Current State] ──> Final Reconstructed State (Balance: $1030)
Practical Example
Here is a Java implementation of an Event-Sourced bank account aggregate that replays historical events to compute its balance and applies validation checks:
Common Mistakes
- Leaking external side-effects inside event application methods: Triggering a payment API gateway or sending an email inside the
apply()method. Replay loops will cause these emails or API calls to fire repeatedly. Side effects should only execute during command handling. - Ignoring event schema versioning: Modifying field definitions without planning for backwards compatibility. Historically serialized events in the database cannot be changed.
Quick Quiz
Q1: How does snapshotting optimize the startup performance of event-sourced aggregates?
By loading a pre-saved state at a given version number, meaning only events that occurred after the snapshot version need to be replayed.
Q2: Why must applying an event to an aggregate be deterministic?
Because replaying the identical event list must produce the exact same final state. If event applications rely on system clocks or random values, state drift occurs.
Scenario-Based Challenge
The Challenge: Modifying a field name in an event that has been stored 5 million times in the database
You need to change the field cust_id to customerId in your primary event schema. However, there are over 5 million historical serialized events in your production event store using the old name. How do you roll out this migration?
Solution Tip: Use an **Upcaster**. When reading events from the store, pass the raw JSON/binary through an upcaster class that intercepts old event types, translates the JSON structure on-the-fly to the new format, and yields the deserialized new model.
Debugging Exercise
Two concurrent orders for the same item check out simultaneously. Both aggregates read inventory level 1, approve the checkout command, write two ItemPurchased events, and sell an item that only had one unit in stock.
The Fix: Implement **Optimistic Concurrency Control (OCC)**. Ensure that the event store database validates the current version on write. When appending events, assert that the version in the database matches the version read during command validation. If another process has written an event and bumped the version, abort the append and retry the transaction.
Interview Questions
1. What is an event store and how does it differ from traditional databases?
An event store is an append-only database specifically optimized to record events. It does not support arbitrary updates or deletes on historical records and requires high-performance primary-key index lookup for aggregate streams.
2. What is event upcasting and when is it used?
Upcasting is a translation pattern that upgrades event models dynamically from old schemas to new schemas at runtime during the read projection phase, preventing the need for massive data migrations in event logs.
Production Considerations
Use Kafka's log compaction sparingly as an event store. Compacted logs delete intermediate events, destroying historical audit trails. Use standard database files for the write store, and stream replicas to Kafka topics.