Beyond State Overwriting: Implementing Event Sourcing in Python
Overview of Event Sourcing
Most applications function by overwriting state. When a player picks up a sword in a game, a database record changes from two to three. This approach is efficient but destructive; it discards the history of how that state was reached. Event Sourcing flips this paradigm. Instead of storing the final balance or the current inventory count, you store a sequence of immutable events—facts that have happened in the past.
By replaying these events from the beginning, you can reconstruct the state at any point in time. This provides an inherent audit log, simplifies debugging by allowing "time travel," and enables the creation of multiple "projections" or views of the same data without altering the source of truth. It is the same fundamental logic that powers Git and Blockchain.

Prerequisites
To follow this implementation, you should have a solid grasp of Python 3.10+ fundamentals, specifically Object-Oriented Programming (OOP). Familiarity with dataclasses and enum is essential, as these provide the structure for immutable events. A basic understanding of Dependency Injection will also help when connecting the inventory logic to the underlying event store.
Key Libraries & Tools
- enum: Used to define distinct, readable event types like
ITEM_ADDEDandITEM_REMOVED. - dataclasses: Provides a concise way to create event objects, specifically using
frozen=Trueto ensure immutability. - collections: A specialized dictionary subclass for counting hashable objects, used here to aggregate inventory totals.
- functools: Implements a simple memoization strategy to avoid replaying the entire event history on every read request.
- Flox: A tool for creating reproducible development environments, ensuring consistent package management across different machines.
Code Walkthrough: Building the Core System
Step 1: Defining Immutable Events
We start by defining what an event looks like. It must contain the type of action and the data associated with it.
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum, auto
class EventType(Enum):
ITEM_ADDED = auto()
ITEM_REMOVED = auto()
@dataclass(frozen=True)
class Event:
type: EventType
data: str
timestamp: datetime = field(default_factory=datetime.now)
The frozen=True parameter is vital. Events represent the past; they cannot be changed once they occur. We use a default_factory for the timestamp to ensure each event is accurately placed in the timeline.
Step 2: The Event Store and Caching
The Event Store is a simple append-only list. To prevent performance degradation as the list grows, we apply a cache to the state reconstruction method.
from functools import cache
from collections import Counter
class Inventory:
def __init__(self, store):
self.store = store
@cache
def get_items(self):
counts = Counter()
for event in self.store.get_all_events():
if event.type == EventType.ITEM_ADDED:
counts[event.data] += 1
elif event.type == EventType.ITEM_REMOVED:
counts[event.data] -= 1
return {k: v for k, v in counts.items() if v > 0}
def _invalidate_cache(self):
self.get_items.cache_clear()
When we add an item, we append an event to the store and trigger _invalidate_cache(). The next time get_items() is called, it recalculates and recaches the state.
Step 3: Advanced Projections
Projections allow us to ask different questions of our data. For example, we can determine which items were collected most frequently, regardless of whether they are still in the inventory.
def get_most_collected(store):
events = store.get_all_events()
added_items = [e.data for e in events if e.type == EventType.ITEM_ADDED]
return Counter(added_items).most_common(3)
Syntax Notes
This implementation relies on Generic Type Variables (TypeVar) when evolving the system to handle complex objects rather than just strings. Using typing.Generic[T] allows the Event and EventStore classes to remain flexible, supporting any data structure while maintaining type safety. The use of the decorator pattern via @cache demonstrates a clean way to separate performance concerns from business logic.
Practical Examples
- Financial Systems: Storing every transaction (credit/debit) instead of just the balance to provide a perfect audit trail.
- E-commerce: Tracking how long items sit in a cart before being removed to analyze user hesitation.
- Gaming: Building a replay system by storing player inputs as events to recreate the match exactly.
Tips & Gotchas
- Schema Evolution: If you change the structure of your
Itemobject later, your old events might break. You must plan for "upcasting" (transforming old events into the new format) or versioning your event schemas. - Snapshotting: For systems with millions of events, replaying from zero is too slow even with local caching. Periodically save a "snapshot" of the state so you only have to replay events from the last snapshot forward.
- Avoid for CRUD: If your application only requires basic create, read, update, and delete operations without any need for history, event sourcing will introduce unnecessary complexity.
- dataclasses
- 17%· libraries
- enum
- 17%· libraries
- Blockchain
- 8%· concepts
- collections
- 8%· libraries
- Event Sourcing
- 8%· concepts
- Other topics
- 42%

Stop Overwriting State And Use Event Sourcing Instead
WatchArjanCodes // 25:12
On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!