Entities
DDD CQRS ES
Aggregates cluster multiple domain elements together to represent a concept. They are usually composed of two kinds of elements - those with unique identities (Entities) and those without (Value Objects).
Entities represent unique objects in the domain model just like Aggregates, but they don't manage other objects. Just like Aggregates, Entities are identified by unique identities that remain the same throughout its life - they are not defined by their attributes or values. For example, a passenger in the airline domain is an Entity. The passenger's identity remains the same across multiple seat bookings, even if her profile information (name, address, etc.) changes over time.
Note
In Protean, Aggregates are actually entities that have taken on the additional responsibility of managing the lifecycle of one or more related entities.
Definition
An Entity is defined with the Domain.entity decorator:
from protean.domain import Domain
from protean.fields import Date, String
publishing = Domain(__name__)
@publishing.aggregate
class Post:
name: String(max_length=50)
created_on: Date()
@publishing.entity(part_of=Post)
class Comment:
content: String(max_length=500)
An Entity has to be associated with an Aggregate. If part_of is not
specified while defining the identity, you will see an IncorrectUsageError:
>>> @publishing.entity
... class Comment:
... content = String(max_length=500)
...
IncorrectUsageError: 'Entity `Comment` needs to be associated with an Aggregate'
An Entity cannot directly enclose an Aggregate. Trying to do so will
throw IncorrectUsageError.
However, entities can enclose other entities using HasOne and HasMany
relationships, as described in the Associations section below.
Configuration
Similar to an aggregate, an entity's behavior can be customized with by passing
additional options to its decorator, or with a Meta class as we saw earlier.
Available options are:
abstract
Marks an Entity as abstract if True. If abstract, the entity cannot be
instantiated and needs to be subclassed.
auto_add_id_field
If True (the default), Protean automatically adds an identifier field
(acting as primary key) to the entity. Set to False to suppress automatic
identity generation — useful when the entity defines its own explicit
identifier field.
schema_name
The name to store and retrieve the entity from the persistence store. By
default, schema_name is the snake case version of the Entity's name.
database_model
Similar to an aggregate, Protean automatically constructs a representation of the entity that is compatible with the configured database. While the generated model suits most use cases, you can also explicitly construct a model and associate it with the entity, just like in an aggregate.
provider
Inherited from the parent aggregate. Entities are always persisted in the same persistence store as their aggregate — you cannot configure a separate provider for an entity.
limit
The maximum number of entity instances returned by default queries
(default: 100). Set to None or a negative value to remove the limit.
Note
An Entity is always persisted in the same persistence store as its Aggregate.
Entity Lifecycle
Protean tracks the lifecycle state of every entity instance internally. The state determines what happens when the aggregate is persisted:
| State | Property | Meaning |
|---|---|---|
| New | _state.is_new |
Freshly constructed, not yet persisted. Will be inserted on save. |
| Persisted | _state.is_persisted |
Loaded from or saved to the database. No pending changes. |
| Changed | _state.is_changed |
Modified since last persistence. Will be updated on save. |
| Destroyed | _state.is_destroyed |
Marked for deletion. Will be removed on save. |
State transitions happen automatically — you don't need to manage them directly. Creating an entity marks it as new; modifying an attribute marks it as changed; removing it from a collection marks it as destroyed; persisting the aggregate marks surviving entities as persisted.
Raising Events from Entities
Entities can raise domain events using the raise_() method, just like
aggregates. However, the event is always registered on the aggregate
root, not on the entity itself — the root is the owner of the event
stream.
@domain.entity(part_of=Order)
class OrderItem:
product_name: String(max_length=100)
quantity: Integer()
def update_quantity(self, new_qty):
self.quantity = new_qty
self.raise_(OrderItemQuantityChanged(
order_id=str(self._owner.id),
product_name=self.product_name,
new_quantity=new_qty,
))
The event must be associated with the aggregate (part_of=Order), not
with the entity. Access the owning aggregate via self._owner.
Invariants
Entities support the same invariant mechanism as aggregates — use
@invariant.post to enforce rules that must always hold:
@domain.entity(part_of=Order)
class OrderItem:
product_name: String(max_length=100)
quantity: Integer()
@invariant.post
def quantity_must_be_positive(self):
if self.quantity is not None and self.quantity <= 0:
raise ValidationError(
{"quantity": ["Quantity must be positive"]}
)
Entity invariants are checked whenever entity state changes. They work alongside aggregate-level invariants — both must pass for the aggregate cluster to be in a valid state. See the Invariants guide for details.
The defaults() Hook
Override the defaults() method to set computed defaults that depend on
other field values:
@domain.entity(part_of=Order)
class OrderItem:
product_name: String(max_length=100)
quantity: Integer(default=1)
unit_price: Float()
line_total: Float()
def defaults(self):
if self.line_total is None and self.unit_price is not None:
self.line_total = self.quantity * self.unit_price
defaults() runs during initialization, after all field values have been
set but before invariants are checked. Aggregates, entities, and value
objects all support this hook.
Persistence
Entities are always persisted as part of their parent aggregate's transaction. In relational databases (SQLAlchemy provider), each entity type gets its own table with a foreign key back to the aggregate. In document databases (Elasticsearch provider), entities are typically stored as nested documents within the aggregate's document.
You never persist an entity directly — always persist through the aggregate's repository:
repo = domain.repository_for(Order)
repo.add(order) # Persists the order AND all its OrderItems
Associations
Entities can enclose other entities within them using HasOne and HasMany relationships, similar to aggregates. Additionally, entities automatically receive Reference fields that establish inverse relationships to their parent aggregate.
Automatic Reference Fields
When an entity is associated with an aggregate, Protean automatically creates a Reference field that points back to the parent:
@domain.aggregate
class Order:
number: String(max_length=20)
items = HasMany("OrderItem")
@domain.entity(part_of=Order)
class OrderItem:
product_name: String(max_length=100)
quantity: Integer()
# Automatically gets: order = Reference(Order)
# Automatically gets: order_id = String() # Shadow field
Explicit Reference Fields
You can also explicitly define reference fields for more control:
@domain.entity(part_of=Order)
class OrderItem:
product_name: String(max_length=100)
quantity: Integer()
order = Reference(Order, referenced_as="order_number")
# Creates shadow field 'order_number' instead of 'order_id'
Navigation Between Entities
Reference fields enable navigation from child entities back to their parent aggregate:
# Access parent aggregate from entity
order_item = OrderItem(product_name="Widget", quantity=2)
parent_order = order_item.order # Order object
order_id = order_item.order_id # Order's ID value
For comprehensive relationship documentation, see Expressing Relationships and Association Fields.
Common Errors
| Exception | When it occurs |
|---|---|
IncorrectUsageError |
Entity defined without part_of — every entity must be associated with an aggregate. |
ValidationError |
Field validation fails during construction (e.g. missing required field). Contains a messages dict. |
ValidationError |
An @invariant.post check on the entity raises a validation error. |
IncorrectUsageError |
Trying to instantiate an abstract entity directly. |
ConfigurationError |
Entity raises an event not associated with its aggregate root (part_of mismatch). |
See also
Concept overview: Entities — What entities are and how they relate to aggregates.
Decision guidance: Choosing Element Types — When to use an entity vs. an aggregate vs. a value object.
Related guides:
- Invariants — Enforcing business rules on entities and aggregates.
- Raising Events — How entities raise events through their aggregate root.
- Expressing Relationships — Full relationship and association documentation.
Patterns:
- Design Small Aggregates — Drawing the right boundaries between aggregates and entities.
- Encapsulate State Changes — Named methods for controlled mutation.