Hello! Welcome back to our course on Domain-Driven Design.
Introduction
In our last lesson, we focused on implementing Value Objects—immutable objects defined by their attributes, like a Price or an Address. We used Python's @dataclass(frozen=True) to create robust, self-validating, and immutable representations of these concepts.
Today, we turn our attention to the other fundamental building block of a domain model: the Entity. While Value Objects represent descriptive aspects of the domain, Entities represent things with a distinct identity and a lifecycle.
Our learning outcome for this lesson is to implement Entities in Python, ensuring they have a stable identity throughout their lifecycle.
This lesson will cover:
- The core characteristics of an Entity: identity, mutability, and lifecycle.
- Strategies for managing an Entity's unique and stable identity.
- The critical implementation pattern of separating creation from instantiation.
- How to implement an Entity in Python, encapsulating business logic within its methods.
1. What is an Entity?
An Entity is an object whose definition is not based on its attributes, but on a thread of continuity and identity. Think of a Customer in an e-commerce system. The customer might change their shipping address, update their name, or modify their payment methods over time. Despite these changes to its attributes, it is still the same customer. This "sameness" is defined by a unique identifier.
Key characteristics of an Entity:
- Identity: It has a unique ID that distinguishes it from all other objects of the same type.
- Mutability: Its state and attributes can change over its lifetime.
- Lifecycle: It is created, goes through various state changes, and may eventually be archived or deleted.
To solidify this concept, let's start with a clear definition.
Basic Building Blocks DDD - Entities
The article 'Basic Building Blocks DDD - Entities' by Szymon Miks provides a concise definition of an Entity and a helpful analogy.
Please read the section 'What is an entity?'. Pay close attention to the example of the 'Citizen' to understand how an object's identity remains constant even when its attributes change.
The core takeaway is that for an Entity, identity is paramount. Two Customer entities are different even if they share the same name and address, because their customer_id is different. This is the opposite of Value Objects, where two Address objects are considered the same if their street, city, and state are identical.
2. Managing a Stable Identity
Since identity is the defining characteristic of an Entity, how we create and manage it is a critical design decision. The identity must be unique and, most importantly, stable for the entire life of the Entity.
There are two main approaches to generating identifiers:
-
Natural Identifiers: Using an attribute that is already part of the domain model (e.g., a user's email address, a book's ISBN). This is often tempting but can be problematic. What happens if the user changes their email address? If the email is the ID, you either have to forbid the change or perform a complex and risky operation to update the identity everywhere it's referenced.
-
Synthetic (or Surrogate) Identifiers: Using a generated identifier that has no business meaning. This is the strongly preferred approach in DDD. Examples include:
- UUIDs (Universally Unique Identifiers): Can be generated by the application without needing to coordinate with a database. Excellent for distributed systems.
- Database Sequences: An auto-incrementing integer managed by the database. Simple and effective for many applications.
By using a synthetic ID, you decouple the Entity's identity from its mutable attributes, ensuring the ID remains stable forever.
This resource, 'Entities' from lab.abilian.com, offers a clear explanation of why synthetic identifiers are usually the better choice.
Read the section titled 'Managing identity'. Focus on the different ways to generate synthetic identifiers and the reasoning for preferring them over natural identifiers.
3. Implementation: Creation vs. Instantiation
This is one of the most important and subtle aspects of implementing Entities correctly. We must distinguish between the act of creating a new entity and instantiating an existing one from storage.
- Creation: This happens only once in an entity's life. It involves generating a new unique ID, validating the initial state, and establishing its invariants (the business rules that must always be true). This logic should be handled by a Factory. A factory can be a simple function or a dedicated class method.
- Instantiation (or Reconstitution): This can happen many times. It's the process of loading an existing entity's data (including its ID) from a database or other persistence mechanism and creating a Python object in memory. The
__init__method is perfect for this. It should be simple, just assigning the provided state, because we assume the data is already valid (it was validated at creation).
This separation is crucial. You don't want to re-run creation logic (like generating a new ID) every time you load a user from the database.
The following video explains this concept and shows how to structure the code in Python.
Domain-Driven Design (DDD) Patterns in Python
In this segment from his talk 'Domain-Driven Design (DDD) Patterns in Python', Robert Smallshire provides an excellent explanation of the creation vs. instantiation pattern.
Watch from 18:14 to 21:40. Pay close attention to: The characteristics of an Entity (identity, lifecycle). The distinction between a factory function for creation and __init__ for instantiation. The recommendation to use 'factless' IDs like UUIDs.
4. Implementing an Entity in Python
Now, let's bring these concepts together into a concrete Python implementation. An Entity class should:
- Have its identity passed in during initialization.
- Protect its identity from being changed.
- Implement
__eq__and__hash__based only on its identity. - Encapsulate business logic and state changes in methods.
Let's look at a practical example.
Domain-Driven Design (DDD) Patterns in Python
Continuing with the same video, let's see how these ideas translate into code.
Watch from 21:40 to 23:40. Notice how the Entity base class handles common machinery and how the Customer entity subclasses it. Pay attention to the register_customer factory function, which gives a name to the creation process that comes from the Ubiquitous Language.
Let's build our own Customer entity step-by-step, incorporating these patterns.
First, we define our identity as a Value Object, which is a common and good practice.
import uuid
from dataclasses import dataclass
@dataclass(frozen=True)
class CustomerId:
value: uuid.UUID
Now, the Customer Entity itself. Notice the __init__ is for instantiation, and the business logic is in methods like change_name.
class Customer:
def __init__(self, id: CustomerId, name: str, email: str):
# __init__ is for instantiating an existing entity.
# It just assigns values. No logic, no ID generation.
self._id = id
self._name = name
self._email = email
self._is_active = True
# The ID is exposed via a read-only property to ensure it's stable.
@property
def id(self) -> CustomerId:
return self._id
@property
def name(self) -> str:
return self._name
# Equality is based SOLELY on identity.
def __eq__(self, other):
if not isinstance(other, Customer):
return NotImplemented
return self.id == other.id
def __hash__(self):
return hash(self.id)
# Business logic is encapsulated in methods.
def change_name(self, new_name: str):
if not new_name:
raise ValueError("Customer name cannot be empty.")
self._name = new_name
# In a real system, this might also publish a Domain Event.
def deactivate(self):
self._is_active = False
Finally, we create a factory function to handle the creation of a new customer.
# This is a factory. It handles the CREATION of a new customer.
def register_customer(name: str, email: str) -> Customer:
# 1. Perform validation for creation.
if not name or not email:
raise ValueError("Name and email are required to register a customer.")
# 2. Generate a new, unique, factless ID.
customer_id = CustomerId(uuid.uuid4())
# 3. Create the new entity instance.
customer = Customer(id=customer_id, name=name, email=email)
# In a real system, you might also raise a CustomerRegistered event here.
return customer
# --- Usage ---
# 1. Creating a NEW customer using the factory
new_customer = register_customer(name="Alice", email="alice@example.com")
print(f"Registered new customer with ID: {new_customer.id.value}")
# 2. Changing the entity's state via its methods
new_customer.change_name("Alice Smith")
print(f"Customer's new name: {new_customer.name}")
# 3. Re-instantiating an EXISTING customer (e.g., from a database)
# We use the constructor directly with the existing ID.
existing_id = new_customer.id
reconstituted_customer = Customer(id=existing_id, name="Alice Smith", email="alice@example.com")
# They are considered equal because their IDs are the same.
print(f"Are they the same customer? {new_customer == reconstituted_customer}")
This example clearly separates the concerns of creation (factory) and instantiation (__init__), while ensuring the Entity's identity remains stable and its business rules are encapsulated in its methods.
Conclusion
In this lesson, we've explored the theory and practice of implementing Entities, one of the core patterns in Domain-Driven Design.
Key Takeaways:
- Entities are defined by identity, not attributes. An Entity maintains its identity throughout its lifecycle, even as its state changes.
- Use synthetic, factless IDs. Prefer UUIDs or database sequences for your Entity IDs to ensure they are stable and decoupled from business attributes.
- Separate creation from instantiation. Use a Factory to handle the creation of new Entities (generating IDs, validation). Use the
__init__method only for reconstituting existing Entities from a data source. - Encapsulate logic. Business rules and state transitions should be implemented as methods on the Entity itself, ensuring it always remains in a valid state.
- Identity determines equality. The
__eq__and__hash__methods for an Entity should be based exclusively on its unique identifier.
Preview of the Next Lesson:
We now have two building blocks: Value Objects and Entities. However, a real-world business operation, like placing an order, might involve changes to multiple related objects (e.g., an Order and its OrderLine items). How do we ensure these changes are consistent and atomic? In the next lesson, we will introduce the Aggregate pattern, which groups related entities and value objects into a single unit of consistency.