Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our module on State Management with Event Sourcing.
In our last lesson, we designed the core components of an event-sourced system: the FXTrade aggregate root and its associated domain events. We established that command methods on the aggregate enforce business rules and produce events, while a private _apply method is responsible for mutating the aggregate's state based on those events.
Today, we will put that design to work. Our learning outcome is to implement event replay to reconstruct the current state of an aggregate from its historical event stream. This process, often called "rehydration," is the fundamental mechanism for bringing an aggregate to life from its persisted history, making it ready to process new commands.
1. The "Why" and "How" of Event Replay
In a traditional, state-oriented system, loading an object is simple: you execute a SELECT * FROM trades WHERE id = ? and map the resulting row to an object. In event sourcing, the "current state" doesn't exist in a single row; it is the cumulative result of all historical events.
Therefore, to load an aggregate, we must:
- Fetch the complete, ordered stream of events for a specific aggregate ID from our event store.
- Instantiate a "blank" version of our aggregate object.
- Iterate through the historical events one by one, from the first to the last, and apply each one to the aggregate object.
The key to this process is the _apply method we designed previously. It acts as a state transition function, ensuring that each event correctly modifies the aggregate's state. After the final event is replayed, the object in memory represents the true current state of the aggregate.
2. A Practical Implementation from Scratch
Let's see this in action. The following video provides a clear, step-by-step implementation of a basic event sourcing system in Python. It demonstrates exactly how to build an object's state by replaying events.
Stop Overwriting State And Use Event Sourcing Instead
This video from ArjanCodes, titled 'Stop Overwriting State And Use Event Sourcing Instead', builds a simple inventory management system from the ground up. It's an excellent practical demonstration of the event replay mechanism.
Please watch from the beginning up to the 12:43 mark. Pay close attention to: The basic Event and EventStore classes (a simple list in this case). How the Inventory class is initialized with the event store but holds no state itself. The implementation of the get_items method. This is the core of event replay: it starts with an empty Counter and iterates through all events in the store to calculate the current inventory.
The get_items method in the video is a perfect, albeit simple, example of event replay. Each time it's called, it reconstructs the state from scratch. The logic for applying events (if event.type == ...) is contained entirely within that method.
3. Formalizing the Pattern: The apply Method
While the video's approach works, in more complex systems it's better to formalize how state is rebuilt. The logic for applying an event shouldn't be hidden inside a specific getter method. Instead, it should be a core capability of the aggregate itself. This is where the _apply method pattern we discussed in the last lesson becomes crucial.
Let's look at a code example that implements this more formal pattern.
Event Sourcing: Pattern Implementation - Tutorial | Krython
This tutorial from Krython, 'Event Sourcing: Pattern Implementation', provides a Python implementation of a ShoppingCart aggregate that uses a dedicated method for state reconstruction.
Please review the code for the ShoppingCart class. Focus on the __init__, _rebuild_state, and _apply_event methods. Notice how the constructor immediately calls _rebuild_state, which fetches all events and replays them using _apply_event.
This ShoppingCart example demonstrates a robust pattern:
- The
__init__method orchestrates the state loading. - The
_rebuild_statemethod is responsible for fetching events. - The
_apply_eventmethod contains the state transition logic, just like the_applymethod we designed for ourFXTradeaggregate.
4. Applying the Pattern to Our FXTrade Aggregate
Let's integrate this more formal replay mechanism into our FXTrade aggregate. A common and clean way to do this is by creating a class method that acts as a factory for reconstructing the aggregate from its history.
Building on the pseudo-code from our last lesson, we can add a replay method:
# --- Assume Event classes like FXTradeInitiated, FXTradeBooked are defined ---
class FXTrade:
def __init__(self):
# Initial state is empty. It will be built up by applying events.
self.id = None
self.status = None
self.currency_pair = None
self.dealt_amount = None
# ... other state fields
self.version = 0
self._uncommitted_events = []
# --- Command Methods (Business Logic) ---
# @staticmethod
# def initiate(...): ...
# def book(...): ...
# def settle(...): ...
# --- State Reconstruction ---
@classmethod
def replay(cls, events: list):
"""
Factory method to reconstruct an aggregate from its historical event stream.
"""
trade = cls() # 1. Create a new, empty instance
for event in events: # 2. Iterate through historical events
trade._apply(event) # 3. Apply each event to mutate state
# After replay, the list of uncommitted events should be empty
trade._uncommitted_events = []
return trade
# --- State Mutation ---
def _apply(self, event):
"""
Applies an event to mutate the aggregate's state.
This method contains no business logic, only state changes.
"""
if isinstance(event, FXTradeInitiated):
self.id = event.trade_id
self.status = 'INITIATED'
self.currency_pair = event.currency_pair
self.dealt_amount = event.dealt_amount
elif isinstance(event, FXTradeBooked):
self.status = 'BOOKED'
# ... update rate, counter_amount, etc.
elif isinstance(event, FXTradeSettled):
self.status = 'SETTLED'
# This version increment happens for both historical and new events
self.version += 1
With this structure, the process of loading an aggregate before handling a command becomes very clear. The article "How I Learned to Stop Worrying and Love Raw Events" provides an excellent example of this flow within a command handler.
How I Learned to Stop Worrying and Love Raw Events ...
This article provides a practical look at how event replay fits into the overall command processing pipeline.
Please read the sections 'Command Handlers: Orchestration Logic' and 'Aggregates: Pure Python Business Logic'. Focus on the code inside the ChangePasswordCommandHandler. You'll see the exact sequence we've been discussing: Get events from the store. Rebuild the aggregate state by iterating and calling user.apply(event). Execute the business logic on the now-rehydrated aggregate.
This example solidifies the pattern: Replay, then Act. You must first reconstruct the past (replay events) to earn the right to decide the future (execute a command).
5. A Note on Performance
Your experience with large-scale systems at Revolut has likely made you consider the performance implications of this pattern. What happens when an aggregate, like a long-lived client account, has thousands or even millions of events? Replaying the entire history every time you need to load the account would be prohibitively slow.
This is a well-known challenge in event sourcing. The primary solution is snapshotting.
A snapshot is a serialized copy of the aggregate's full state at a specific version (e.g., after event #1000). To reconstruct the aggregate, you would:
- Load the most recent snapshot.
- Fetch and replay only the events that have occurred since that snapshot was taken.
This dramatically reduces the number of events that need to be processed. The ArjanCodes video you watched earlier briefly touches on a related concept with its cache decorator (from timestamp 12:52), which is essentially a form of in-memory snapshotting. We will dive deep into implementing a persistent snapshot strategy in our next lesson.
Conclusion
Today, we have implemented the core mechanism that makes event sourcing work: replaying an event stream to reconstruct an aggregate's state.
Key Takeaways:
- Event Replay is the process of loading an aggregate by starting with a blank instance and applying its historical events in order.
- A dedicated
applymethod is the conventional and robust way to handle state transitions for each event, both during replay and when new events are generated. - The standard workflow for handling a command is to first replay the aggregate's history to get its current state, and then act by executing the command's business logic on the rehydrated object.
- Replaying very long event streams creates a performance bottleneck, a problem that we will solve with snapshots.
Preview of the Next Lesson:
We've identified the performance limitations of event replay. In our next lesson, we will address the learning outcome: "Implement snapshots for an aggregate to optimize state reconstruction performance for long-lived objects." We will design and implement a snapshotting mechanism to ensure our system remains fast and responsive, even for aggregates with extensive histories.