Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Eventsourcing Library vs. Manual Implementation

Hello! Welcome to the first lesson of our new module, "Applying Event Sourcing in Python."

In the previous module, we concluded by analyzing the trade-offs between different persistence technologies for an event store—relational databases, document databases, and specialized event stores. We established that the choice involves balancing consistency, performance, and operational complexity.

This lesson builds directly on that understanding. We've explored the what (the persistence layer); now we'll explore the how (the implementation strategy). We will address the following learning outcome:

Compare the Python 'eventsourcing' library with a manual implementation approach based on criteria such as developer experience, persistence options, and maintainability.

Instead of building everything from scratch, we often use frameworks and libraries to handle complex, recurring problems. Today, we'll evaluate this trade-off in the context of event sourcing. We'll start by examining a "from-scratch" implementation to establish a baseline, and then see how a dedicated library like eventsourcing provides abstractions to solve the hard parts, allowing you to focus on business logic.

1. The Baseline: A Manual Implementation

To have a meaningful comparison, we first need a clear picture of what a manual, or "from-scratch," approach entails. The following video provides an excellent, concise demonstration of building a simple event-sourced system in Python without any specialized libraries.

Stop Overwriting State And Use Event Sourcing Instead

This video from the ArjanCodes channel, 'Stop Overwriting State And Use Event Sourcing Instead', walks through building a basic event-sourced inventory system. It's a perfect example of the manual approach.

Please watch from the beginning until 12:52. Pay close attention to the core components being built: the Event data class, the in-memory EventStore, and the Inventory class which reconstructs its state by iterating through the event list.

As you saw, the core idea is straightforward, but you are responsible for implementing every piece of the puzzle:

  • Event Definition: You create your own event classes, typically using dataclasses.
  • Event Store: You manage the storage of events. In the video, this is a simple in-memory list. For a real application, you would need to write the code to persist this to a database, which, as we saw in the last module, is a non-trivial task involving database connections, table schemas, and transaction management.
  • State Reconstruction: The aggregate (Inventory) is responsible for replaying events to calculate its current state. The logic (for event in self.store.get_all_events()...) is written explicitly within your domain object.

This manual approach gives you complete control, but it also places the burden of implementing all the cross-cutting concerns squarely on your shoulders.

2. The Framework Approach: The eventsourcing Library

Now, let's look at the alternative: using a specialized library. The eventsourcing library for Python aims to provide the "conceptually cohesive mechanism" for event sourcing, as described by Eric Evans in Domain-Driven Design. It handles the generic "how" of persistence and state management so you can focus on the "what" of your domain.

Let's get a high-level overview of the library's philosophy and features.

eventsourcing Documentation

The official documentation for the 'eventsourcing' library provides a concise introduction to its purpose and design.

Please read the 'Introduction' section (1.1), including 'What is event sourcing?', 'This library', 'Features', and 'Design overview'. This will give you a solid understanding of what the library offers out-of-the-box.

The documentation highlights that the library is not just about storing events; it's a framework that provides solutions for snapshotting, versioning, optimistic concurrency control, and notifications for projections—all features that are complex to build and maintain manually.

3. A Head-to-Head Comparison

Let's now compare the two approaches across the key criteria mentioned in our learning outcome.

A. Developer Experience & Domain Modeling

This criterion is about how you, the developer, interact with the code to model your business domain.

  • Manual Approach: As seen in the ArjanCodes video, the domain logic and the mechanics of event sourcing are intertwined. The Inventory class contains explicit loops to process events. While clear, this mixes domain concerns ("what is the count of an item?") with infrastructure concerns ("how do I replay events to find the count?").
  • Library Approach: The eventsourcing library provides base classes like Aggregate and Aggregate.Event. You define your aggregate and its events by subclassing these. The framework handles the mechanics of event creation, state reconstruction (by calling apply() methods on your events or aggregate), and collecting pending events. This leads to cleaner domain code that is more focused on business rules.

To see this in practice, let's examine how the library structures an aggregate.

eventsourcing Documentation

The 'domain — Domain models' section of the documentation shows how to build an aggregate with the library.

Please read section 1.4.3, 'Basic example'. Compare the structure of the World aggregate with the Inventory class from the video. Notice how trigger_event() is used in command methods and how state changes are encapsulated in apply() methods on the event classes.

The library's approach encourages a clean separation of concerns, aligning well with Domain-Driven Design principles. Command methods make decisions and trigger events; the events themselves (or methods on the aggregate dispatched by event type) are responsible for mutating state.

B. Persistence Options

This is arguably where a library provides the most immediate value.

  • Manual Approach: You are entirely responsible for persistence. You must choose a database, design the schema, write the data access code (e.g., using an ORM or raw SQL), and implement optimistic concurrency control. We spent much of the last module exploring the complexities of just this one area.
  • Library Approach: The eventsourcing library completely abstracts the persistence layer. It uses an InfrastructureFactory that can be configured to use different backends without changing your application code. Switching from an in-memory store for testing to a durable PostgreSQL database for production is a matter of setting environment variables.

Let's see how simple the library makes this.

eventsourcing Documentation

The 'Configuring persistence' section of the documentation demonstrates how to switch the underlying database.

Please read section 1.5.7, 'Configuring persistence'. Observe how setting the INFRASTRUCTURE_FACTORY and SQLITE_DBNAME environment variables is all that's needed to switch from the default in-memory store to SQLite.

This abstraction is incredibly powerful. The library handles the low-level details of database-specific SQL, connection management, and table creation, freeing you from writing and maintaining a significant amount of boilerplate infrastructure code.

C. Maintainability & Advanced Features

Long-term maintainability often depends on having robust solutions for advanced, but common, requirements.

  • Manual Approach:

    • Snapshots: As the "Event Sourcing do's and don'ts" video mentions, you'll eventually need snapshots for long-lived aggregates. Manually, this requires designing a snapshot storage strategy and complex logic to load from a snapshot and then replay only the subsequent events.
    • Event Versioning: When your event schemas change, you must write and manage "upcaster" functions to migrate old event data to the new format on the fly. This can become very complex to manage over the life of an application.
    • Notifications for Projections: You need to implement a pattern like the transactional outbox to reliably publish events to downstream consumers (e.g., for building read models).
  • Library Approach: The eventsourcing library provides built-in solutions for these challenges.

    • Snapshots: A simple application.take_snapshot(aggregate_id) method is provided. The repository automatically uses the latest snapshot when retrieving an aggregate, transparently handling the reconstruction logic.
    • Versioning: The library has a formal mechanism for versioning aggregates and events using a class_version attribute and upcast_vX_vY() static methods. The framework automatically applies these upcasters during event replay.
    • Notifications: The library includes a NotificationLog that provides a reliable, pull-based mechanism for projectors to consume events.

To appreciate the library's built-in support, quickly review these features in the documentation.

eventsourcing Documentation

The documentation covers these advanced features, which are significant for maintainability.

Skim through the following sections: 'Snapshotting' (1.5.6) in the 'Applications' documentation and 'Versioning' (1.4.6) in the 'Domain models' documentation. You don't need to understand every detail, but appreciate that a formal, tested solution exists for these problems.

4. Summary of Trade-offs

Let's synthesize our comparison into a table. This summarizes the core trade-offs an architect or developer must consider.

Criterion Manual Implementation eventsourcing Library
Developer Experience High Control, High Boilerplate. Full control over every detail. Logic for mechanics (replay, etc.) is explicit. Convention over Configuration. Focus on domain logic (apply methods). Framework handles mechanics.
Persistence High Effort. You build everything: DB connection, schema, queries, concurrency control, transactions. Low Effort. Abstracted via InfrastructureFactory. Switching DBs is a configuration change.
Maintainability Low (for complex features). Snapshots, versioning, and notifications must be custom-built and maintained. High. Built-in, tested solutions for snapshotting, versioning, concurrency control, and notification logs.
Learning Curve Low (initially). Concepts are simple at first. Complexity grows with the system's needs. Medium. Requires learning the library's abstractions (Aggregate, Application, etc.) and conventions.
Dependencies Minimal. You control all dependencies. Adds a key dependency. You are tied to the library's architecture and release cycle.
Best For Small projects, learning exercises, or highly specialized cases where a library's conventions are too restrictive. Non-trivial applications where developer productivity and long-term maintainability are priorities.

Conclusion

Today we've moved from the theory of event sourcing to the practice of its implementation in Python. We've seen that while a manual approach is feasible and excellent for learning, it quickly requires you to solve complex infrastructure problems that are common to all event-sourced systems.

Key Takeaways:

  • A manual implementation gives you ultimate control but forces you to reinvent the wheel for persistence, concurrency control, snapshotting, and event versioning.
  • The eventsourcing library provides a lightweight framework that handles these common, difficult problems through well-defined abstractions.
  • The primary benefit of the library is accelerated development and improved maintainability by allowing developers to focus on domain logic rather than infrastructure plumbing.
  • The choice is a classic architectural trade-off: the flexibility and control of a manual approach versus the productivity and robustness of a framework-based approach. For most production systems, the value provided by the framework is substantial.

Preview of the Next Lesson:

Now that we understand the value proposition of the eventsourcing library, it's time to get hands-on. In the next lesson, we will set up the eventsourcing library with a message broker integration for event notification, taking our first practical step in building a robust, event-driven application with the framework.

Can't find a good explanation? Sign up and we'll make it for you

Sign up