Skip to main content

Welcome to a Grasp lesson

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

Lesson 21

Implementing Immutable Domain Events in Python

Recommended resources

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

In our last lesson, we established the conceptual foundation for Domain Events. We learned that they are immutable records of significant business occurrences, and their primary purpose is to decouple different parts of our system.

Today, we move from theory to practice. Our goal is to implement immutable Domain Event objects in Python. We'll learn the best modern Python techniques to create these objects, ensuring they are robust, clear, and truly represent unchangeable facts about our domain.

Why Immutability is Non-Negotiable

Before we write any code, let's reinforce why immutability is a critical characteristic of a domain event. An event is a report of something that has already happened. If you can change the event after it has been created, you are effectively rewriting history. This can lead to subtle bugs, data corruption, and a system that is impossible to audit or trust.

Imagine an OrderBilled event. If some other part of the code could later change the amount_billed on that event object, any downstream system that reacts to it (like accounting or shipping) might act on incorrect information.

To see a concrete example of this pitfall and its solution, let's look at a short article.

Event Sourcing: Pattern Implementation - Tutorial | Krython

This article, 'Event Sourcing: Pattern Implementation', has a great section that directly addresses the dangers of mutable events and shows the correct, immutable approach.

Please read the section titled 'Pitfall 1: Mutable Events'. It's very short and clearly demonstrates the problem and introduces the frozen=True solution, which we'll be exploring next.


The Modern Python Solution: Dataclasses

As the article suggests, the key to immutability in this context is @dataclass(frozen=True). But what are dataclasses?

As a software engineer, you've likely written many classes that are primarily used to hold data. This often involves writing boilerplate methods like __init__, __repr__, and __eq__. Python's dataclasses module automates all of this for you, making your code cleaner and less error-prone.

Let's watch a video that perfectly illustrates the problem dataclasses solve and how they work.

Python dataclasses will save you HOURS, also featuring attrs

This video, 'Python dataclasses will save you HOURS' from the mCoding channel, provides an excellent and practical introduction. It will show you just how much boilerplate code we can eliminate.

Please watch from the beginning until 00:04:17. This covers: The pain of manually creating a data class (00:00:58 - 00:03:23). The simple and clean dataclass solution (00:03:12 - 00:04:17). Focus on the contrast between the two approaches.


Implementing Immutable Events with frozen=True

Now for the crucial part. Dataclasses provide a simple, declarative way to make an object immutable: the frozen=True parameter. When you set this, dataclasses will prevent any changes to the object's attributes after it has been initialized, which is exactly what we need for our domain events.

Let's return to the mCoding video to see this in action.

Python dataclasses will save you HOURS, also featuring attrs

The next segment of the video focuses specifically on creating immutable, hashable objects.

Watch from 00:04:17 to 00:05:07. This part explains how frozen=True works and shows the FrozenInstanceError you get when trying to modify an attribute. This is the core mechanism we'll use for our domain events.


The Anatomy of a Domain Event in Code

With @dataclass(frozen=True) as our tool, we can now define a clear structure for our domain events. A common and effective pattern is to create a base DomainEvent class that all specific events will inherit from. This base class can hold common information, like a timestamp.

Let's examine a complete, professional implementation.

Decoupling Business Logic with Domain Event Dispatch ...

The article 'Decoupling Business Logic with Domain Event Dispatch ...' contains a perfect code snippet showing how to define a base event and a specific event using dataclasses.

In the provided code block, focus on the file named events.py. Notice how DomainEvent is defined as a frozen dataclass with a timestamp, and UserRegisteredEvent inherits from it, adding its own specific, immutable fields.

Let's break down the structure from that example:

from dataclasses import dataclass
from datetime import datetime

# 1. The Base Event
@dataclass(frozen=True)
class DomainEvent:
    """A base class for all domain events."""
    occurred_on: datetime

# 2. A Specific Event
@dataclass(frozen=True)
class UserRegisteredEvent(DomainEvent):
    """Raised when a new user completes registration."""
    user_id: str
    username: str
    email: str

This structure is clean, expressive, and robust:

  • Immutability: frozen=True ensures that once an event like UserRegisteredEvent is created, it cannot be altered.
  • Clarity: The class name UserRegisteredEvent follows the "past-tense verb" convention. The attributes (user_id, username, email) clearly define the data payload.
  • Consistency: The base class ensures every event in your system will have an occurred_on timestamp.
  • Type Safety: Using explicit fields with type hints (user_id: str) makes your code easier to understand and allows static analysis tools to catch errors.

When you create an instance of this event, you'd do it like this:

# In a real application, this would happen inside your domain model
event = UserRegisteredEvent(
    occurred_on=datetime.utcnow(),
    user_id="user-123",
    username="alex",
    email="alex@example.com"
)

# This would raise a FrozenInstanceError:
# event.username = "alex_new" 

This simple, powerful pattern is all you need to correctly define immutable domain events in your Python applications.


Conclusion

In this lesson, we've bridged the gap between the concept of a domain event and its practical implementation. You now have a solid pattern for creating these crucial objects in your DDD projects.

Key Takeaways:

  • Domain events must be immutable to serve as a reliable record of the past.
  • Python's @dataclass(frozen=True) is the ideal tool for creating immutable data objects with minimal boilerplate.
  • A good pattern is to have a DomainEvent base class with common fields (like a timestamp) and specific event classes that inherit from it.
  • Event classes should have clear, past-tense names and a data payload that provides all necessary context for subscribers.

Next Steps:

We now know how to define an event. The next logical question is: where and when are these events created? In our next lesson, "Raise domain events from within an Aggregate Root following a state change," we will connect events back to our domain model and learn how to generate them as a result of business operations.