Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 22

Raising Domain Events from Aggregate Roots

Recommended resources

Hello! Welcome back to our course on Domain-Driven Design in Python.

In our last lesson, we focused on how to define and implement immutable DomainEvent objects using Python's dataclasses. We established that an event is an unchangeable record of something significant that has already happened in the past.

Today, we'll answer the next logical question: where and when are these events created? The answer lies at the heart of our domain model: the Aggregate Root.

Our learning outcome for this lesson is to raise domain events from within an Aggregate Root following a state change. We will modify our aggregate classes so that when they execute a command that changes their state, they also generate a corresponding event to announce that change.

This is a fundamental step in decoupling our system. The aggregate doesn't need to know who is interested in the change, only that the change occurred.

This diagram shows the core flow we'll be implementing. A command is sent to an Aggregate Root, which processes it, changes its state, and emits one or more events as a result.

1. Why the Aggregate Root is the Source of Events

Before we dive into the code, it's crucial to understand why the Aggregate Root is the designated place for raising events.

In our previous lessons, we defined an Aggregate as a consistency boundary. This means:

  1. The Aggregate Root is the only public entry point for any command that modifies objects within the aggregate.
  2. The Aggregate Root is responsible for enforcing all business rules (invariants) related to its cluster of objects. For example, an Order aggregate might enforce that you cannot add an item to an order that has already been shipped.

A Domain Event represents a fact—a state change that has successfully occurred. Since the Aggregate Root is the sole guardian of state changes and the enforcer of rules, it is the only component that can authoritatively declare that a valid change has happened. An event is, therefore, the by-product of a command successfully passing the aggregate's validation and altering its state.

To deepen this understanding, let's watch a short video that reinforces the role of aggregates in handling state changes.

What makes an Aggregate (DDD)? Hint: it's NOT hierarchy & relationships

The video 'What makes an Aggregate (DDD)?' from the CodeOpinion channel provides excellent insights into why aggregates are designed around state changes and business rules, not just data relationships.

Please watch the following two clips: 5:15 - 6:12: This part explains that aggregates are consistency boundaries used to enforce invariants during state changes (commands). 6:43 - 7:28: This segment directly discusses publishing a domain event from an aggregate after a state change occurs. Focus on the connection the speaker makes between state changes, invariants, and publishing events.


2. The Mechanism: How to Collect Events

Now that we've established the "why," let's focus on the "how." When a command method on an aggregate is called, it might generate one or more events. These events need to be collected so they can be persisted and dispatched to handlers. This all needs to happen within a single, atomic transaction.

A common and robust pattern to achieve this is:

  1. Add a private list (e.g., _pending_events) to your base Aggregate class.
  2. Create an internal method (e.g., _raise_event or trigger_event) that adds a new event to this list.
  3. Inside a command method, after the state has been successfully changed, call this internal method to record the event.
  4. Provide a public method (e.g., collect_events) that an outer component (like a Unit of Work or Application Service) can call to retrieve all pending events and clear the list for the next operation.

This pattern is language-agnostic. Let's see it demonstrated in another language to prove the point.

How to Use the Domain Event Pattern | DDD, Clean Architecture, .NET 6

This video, 'How to Use the Domain Event Pattern' by Milan Jovanović, shows a clear implementation of this event collection mechanism. Although the code is in C#, the pattern is identical to what we'll build in Python.

Watch from 02:37 to 04:31. Pay attention to how: A list of domain events is added to a base AggregateRoot class. A RaiseDomainEvent method is created to add events to that list. This method is called from within a business method (AcceptInvitation) after the primary logic is complete.


3. Python Implementation Patterns

With the general pattern in mind, let's look at how to implement this idiomatically in Python. We'll use the patterns and examples from the eventsourcing library's documentation, which provides a very clean and well-structured approach.

Domain models — eventsourcing 9.4.6 documentation

The documentation for the eventsourcing library has an excellent page on domain models that details exactly how to trigger events from aggregates. We'll explore a few sections to see different ways of achieving our goal.

You don't need to read this all at once. I will guide you through the relevant sections below. Keep this resource open as a reference.

3.1. The Explicit Approach

First, let's look at a slightly more verbose but very clear implementation. Here, the command method explicitly calls a trigger_event method.

Please read the section "Triggering subsequent events" in the eventsourcing documentation. This section explains the role of the trigger_event() method, which handles creating the event, mutating the aggregate's state, and adding the event to the pending list.

Now, let's see it in a full example. Read the "Simple example" section, which introduces a Dog aggregate.

Notice the structure in the Dog class:

  • The add_trick() command method calls self.trigger_event(...).
  • It passes a specific event class, self.TrickAdded, and the relevant data (trick=trick).
  • The TrickAdded event class has an apply() method that contains the logic to actually change the aggregate's state (aggregate.tricks.append(self.trick)).

This clearly separates the decision to do something (in add_trick) from the application of that decision's outcome (in TrickAdded.apply).

3.2. The Declarative Approach with @event

The explicit approach is clear, but it involves some boilerplate. Python's decorators offer a more concise and elegant solution. The eventsourcing library provides an @event decorator for this purpose.

Now, read the section "The event decorator" and then "The Dog aggregate class revisited".

This is a much more "Pythonic" way of achieving the same result:

from eventsourcing.domain import Aggregate, event

class Dog(Aggregate):
    def __init__(self) -> None:
        self.tricks: list[str] = []

    @event("TrickAdded")
    def add_trick(self, trick: str) -> None:
        self.tricks.append(trick)

Here's what the @event decorator does for you:

  1. It automatically defines an event class named TrickAdded.
  2. It infers the event's attributes from the decorated method's signature (in this case, trick: str).
  3. When dog.add_trick("roll over") is called, it triggers the TrickAdded event.
  4. It uses the body of the add_trick method as the logic to apply the state change (equivalent to the apply method in the explicit example).

This is a powerful pattern that keeps the code that defines the command, the event, and the state change logic all in one place.

3.3. Handling Complex Logic

What if a command method needs to perform validation or some logic before deciding to raise an event? A user can't withdraw money if their balance is too low. An order can't be picked up if it hasn't been confirmed.

The @event decorator should only be used on methods that apply a state change. Any validation or conditional logic should happen in a separate, undecorated method.

Please read the section "Non-trivial command methods". It presents a perfect example with an Order aggregate that has confirm() and pickup() methods.

The pattern here is:

  • A public, undecorated method (pickup) contains the business logic and validation.
  • If validation passes, it calls a "private," decorated method (_pickup) that is responsible for the actual state change.
class Order(Aggregate):
    # ... __init__ ...

    @event("Confirmed")
    def confirm(self, at):
        self.confirmed_at = at

    # Public-facing command method with logic
    def pickup(self, at):
        if self.confirmed_at is None:
            raise AssertionError("Order is not confirmed")
        # Call the private method that triggers the event
        self._pickup(at)

    # "Private" method that applies state and triggers event
    @event("PickedUp")
    def _pickup(self, at):
        self.pickedup_at = at

This cleanly separates the command's validation logic from the event-triggering and state-application logic, which is a crucial pattern for building real-world aggregates.


4. An Alternative Pattern: Returning Events

The pattern of collecting events internally is very common, especially in frameworks that use a Unit of Work. However, it's worth knowing there's another valid approach: having the command method directly return a list of the events it generated.

How I Learned to Stop Worrying and Love Raw Events ...

This article, 'How I Learned to Stop Worrying and Love Raw Events', shows an example of this alternative pattern.

Scroll to the code block under the heading 'Aggregates: Pure Python Business Logic'. Look at the change_password method in the UserAggregate class. Notice how it returns a list containing a PasswordChangedEvent.

The UserAggregate's change_password method looks like this:

class UserAggregate(Aggregate):
    # ...
    def change_password(self, current_password: str, new_password: str) -> List[EventDTO]:
        if not self._verify_password(current_password):
            raise InvalidPasswordError("Current password is incorrect")
        # ... more validation ...
        return [PasswordChangedEvent(...)]

The choice between collecting events internally and returning them is often a design preference:

  • Internal Collection: Keeps method signatures clean (they often return None). Integrates smoothly with a Unit of Work that knows to call collect_events().
  • Returning Events: Makes the output of the command method very explicit. The caller is immediately responsible for handling the returned events.

Both patterns correctly place the responsibility of event creation on the Aggregate Root.


Conclusion

In this lesson, we've connected our domain model directly to the concept of Domain Events. You now have the practical tools to make your aggregates announce significant changes in their state.

Key Takeaways:

  • Aggregates are the source of truth: Because they are consistency boundaries that guard business rules, Aggregate Roots are the only place that can authoritatively create and raise domain events.
  • Events follow state changes: An event is raised after a command has been validated and the aggregate's state has been successfully changed.
  • Events are collected: A common pattern is for the aggregate to collect generated events in an internal list, to be retrieved later by a Unit of Work for atomic persistence and dispatching.
  • Python offers elegant patterns: The @event decorator provides a concise, declarative way to link command methods, event creation, and state changes. For complex logic, use a public/private method pair to separate validation from event application.

Next Steps:

We can now define events and raise them from our aggregates. But what's the point if nothing is listening? In our next lesson, "Implement event handlers (subscribers) that react to domain events," we will build the other side of this communication channel and see how loose coupling is finally achieved.

This diagram previews our next topic. The 'OrderStarted' event raised by the aggregate is picked up by multiple, independent handlers, each responsible for a different piece of follow-up logic.