Skip to main content

Welcome to a Grasp lesson

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

Lesson 23

Handling Domain Events with Subscribers

Recommended resources

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

In our last lesson, we established how to raise domain events from within an Aggregate Root to signal that a significant state change has occurred. This was a crucial first step in creating a reactive, decoupled system. An aggregate now announces what happened, but it doesn't know—nor should it care—who listens.

Today, we will build the other side of that communication channel. We'll create the components that listen for these events and react to them.

Our learning outcome for this lesson is to implement event handlers (subscribers) that react to domain events, enabling loose coupling between different parts of the domain. By the end of this lesson, you'll understand how to wire up these listeners to create a complete, end-to-end flow from a command causing a state change to multiple, independent side effects being triggered.

This diagram illustrates the full flow we are aiming for. A command is processed, leading to an event. This single event can then be picked up by multiple, independent event handlers, each performing a different task.

1. The Publish-Subscribe Pattern: Decoupling in Action

The mechanism that allows events and their listeners to remain independent is a classic software design pattern: Publish-Subscribe (or Pub/Sub).

The core idea is simple:

  • Publishers (in our case, the code that dispatches events from an aggregate) send out messages (events) without any knowledge of who, if anyone, is listening.
  • Subscribers (our event handlers) express interest in specific types of messages (events) without any knowledge of who sends them.
  • A central component, often called a Dispatcher or Event Bus, sits in the middle, routing messages from publishers to the appropriate subscribers.

This separation is what gives us loose coupling. We can add, remove, or change a subscriber without ever touching the publisher's code.

To formalize this, let's define the key components.

Decoupling Business Logic with Domain Event Dispatch ...

The article 'Decoupling Business Logic with Domain Event Dispatch' provides clear, concise definitions for the main components of this pattern.

Please read the short definitions under the 'Core Concepts and Principles' heading for 'Domain Event', 'Event Dispatcher', and 'Event Handler'.

In essence, an Event Handler is a piece of logic (often a function or a method on a class) that is executed in response to a specific event.


2. Building a Simple Event System in Python

To demystify how this works, let's build a simple event dispatcher from scratch. Your background in software engineering will make the underlying data structures and logic here feel very familiar.

This video walks through creating a basic but functional event system in Python. It's a great way to see the core mechanics in action.

EVENTS IS THE SOLUTION. EVENT-DRIVEN ARCHITECTURE IN PYTHON

The video 'EVENTS IS THE SOLUTION. EVENT-DRIVEN ARCHITECTURE IN PYTHON' by Andrey Ivanov provides a clear, practical demonstration of building an event system to decouple parts of an application.

Please watch from 05:12 to 13:30. This segment covers: Building the Event System (05:12 - 10:04): Pay attention to how a simple dictionary is used to store a list of handlers (subscribers) for each event name. Implementing and Dispatching (10:04 - 13:30): Observe how the send_email and notify_friends functions are registered as handlers and how the create_user function is changed to dispatch an event instead of calling those functions directly.

As you saw, the implementation is straightforward:

  • register_event(event_type, handler): Adds a handler (a callable, like a function) to a list associated with an event_type key in a dictionary.
  • dispatch(event_type, data): Looks up the event_type in the dictionary and calls every registered handler, passing in the data.

The result is powerful. The create_user function is now completely decoupled from the send_email and notify_friends functions. It only knows that it needs to announce a user_registered event. We can now add a new handler, for instance, add_user_to_analytics, without modifying create_user at all.


3. A Complete Implementation Example

The video provided a great conceptual overview. Now, let's examine a more structured, file-based example that you might see in a real project.

This article provides a full implementation for a user registration scenario, including the event, dispatcher, handlers, and the service that ties them all together.

Decoupling Business Logic with Domain Event Dispatch ...

Let's return to the article 'Decoupling Business Logic with Domain Event Dispatch'. It contains a complete and practical Python example that builds on the concepts we just saw.

Read the section 'Implementation Example with a Python Backend'. Focus on the code blocks for events.py, event_dispatcher.py, handlers.py, and main.py. Pay attention to how these different files collaborate: handlers.py defines the specific actions to take. main.py registers these handlers with the dispatcher at startup. The register_user endpoint dispatches the event after creating the user.

This example demonstrates a common and effective pattern:

  • Centralized Registration: Handlers are registered in one place, typically when the application starts. This makes it easy to see what events are being handled and by what logic.
  • Single Responsibility Handlers: Each handler (send_welcome_email, log_user_activity) does one thing. This adheres to the Single Responsibility Principle and makes the code easier to test and maintain.
  • Decoupled Service Logic: The register_user function is clean. Its job is to handle the core task of user registration and announce that it happened. It is not burdened with orchestrating all the side effects.

4. Architectural Placement of Event Handlers

So far, our handlers have been simple functions. In a layered architecture like the one we've been discussing, where do they belong?

Event handlers often act as the glue between different parts of your domain or between your domain and the outside world. For example, a handler for an OrderPlaced event might need to use a NotificationService (from the Infrastructure layer) to send an email. Or it might need to use another aggregate's repository to update its state.

Because handlers often orchestrate operations involving infrastructure or cross-aggregate logic, they typically reside in the Application Layer.

This diagram clearly shows the domain raising an event, and the event handlers residing in the application layer, where they can coordinate responses.

Let's watch a clip that explains this reasoning. While the code is in C#, the architectural principle is universal.

May The Power of DOMAIN EVENTS Be With You!

The video 'May The Power of DOMAIN EVENTS Be With You!' from Codewrinkles explains why event handling is not a domain layer responsibility.

Watch from 10:06 to 12:40. The speaker makes a critical point about why event handlers cannot be in the domain layer if they need to interact with infrastructure like a database. He then shows how to implement a handler in the application layer.


5. A Glimpse into Consistency and Transactions

We've successfully decoupled our logic. But this introduces a new, important question: what is the relationship between the original operation (e.g., saving the new Order) and the operations in the event handlers (e.g., sending an email or updating Customer statistics)?

This is a deep topic, but understanding the basic trade-offs is essential. The key decision is when to dispatch the events relative to committing the database transaction for the aggregate that raised them.

  1. Dispatch Before Commit (Immediate Consistency):

    • Start transaction.
    • Load aggregate.
    • Execute command method (state changes, events are collected).
    • Dispatch events. Handlers run and make their own changes.
    • Commit transaction (saves changes from the aggregate AND all handlers).
    • Pro: All work is done in a single, atomic transaction. If any handler fails, the entire operation is rolled back. The system is always strongly consistent.
    • Con: Couples the original operation to the success of all its synchronous handlers. A failure in a non-critical handler (e.g., logging) could fail the entire user registration.
  2. Dispatch After Commit (Eventual Consistency):

    • Start transaction.
    • Load aggregate.
    • Execute command method.
    • Commit transaction (saves only the aggregate's changes).
    • Dispatch events. Handlers run, often in the background or in separate transactions.
    • Pro: The core operation is fast and is not coupled to its side effects. A failure in an email handler doesn't prevent the user from being created. This is more resilient and scalable.
    • Con: The system is only eventually consistent. There's a small window of time where the user is created, but the welcome email hasn't been sent yet. You need mechanisms (like retries or outbox patterns) to handle failures in the handlers.

This is a fundamental design trade-off in distributed and event-driven systems. The following video clip discusses this exact choice.

May The Power of DOMAIN EVENTS Be With You!

Let's return to the 'May The Power of DOMAIN EVENTS Be With You!' video. The final section discusses this exact transactional dilemma.

Watch from 17:10 to 21:01. The speaker clearly explains the two approaches (publishing before vs. after the commit) and their consequences for system consistency.


Conclusion

In this lesson, we completed the event-driven communication loop. We've seen how to create and register event handlers that react to domain events, finally achieving the loose coupling that is a major goal of DDD.

Key Takeaways:

  • Event Handlers are subscribers that execute logic in response to a specific domain event.
  • The Publish-Subscribe pattern, implemented via an Event Dispatcher, allows publishers and subscribers to evolve independently.
  • Handlers often reside in the Application Layer as they orchestrate side effects that may involve infrastructure (repositories, APIs, email services).
  • Each handler should have a single responsibility, making the system more modular and maintainable.
  • The choice of when to dispatch events (before or after the main transaction) is a critical design decision that impacts system consistency, trading off between immediate atomicity and eventual consistency with higher resilience.

Next Steps:

We have now covered the essential building blocks of a domain model—the "tactical patterns" of DDD. We've explored Entities, Value Objects, Aggregates, and now the full cycle of Domain Events and Handlers.

In the next module, we will zoom out. We'll move from tactical design to Strategic Design, starting with how to identify the high-level boundaries within a large system, a concept known as Bounded Contexts. The event-driven patterns we've learned are a primary tool for enabling communication between these contexts.