Entities are domain objects with a unique identity that persists over time, regardless of changes to their attributes.
Core Characteristics
Identity
- Defined by a unique identifier (ID), not by attributes
- Identity remains constant throughout the object’s lifecycle
- Two entities with the same attributes but different IDs are different entities
Mutability
- Entities can change their attributes over time
- State transitions are common
- Identity persists through all changes
Lifecycle
- Created, modified, and eventually deleted/archived
- Tracked through their entire existence
- Often have audit trails (created date, modified date, etc.)
Examples
User Entity
public class User
{
public UserId Id { get; private set; }
public string Name { get; set; }
public Email Email { get; set; }
public DateTime CreatedAt { get; private set; }
// Identity-based equality
public override bool Equals(object obj)
{
if (obj is User other)
return Id.Equals(other.Id);
return false;
}
}Order Entity
- Has unique OrderId
- Attributes change (status, items, total)
- Same order regardless of attribute changes
Product Entity
- Identified by ProductId or SKU
- Price, description, stock can change
- Same product throughout lifecycle
Identity vs. Attributes
Identity-based equality:
User A: { Id: 123, Name: "Alice", Email: "alice@old.com" }
User B: { Id: 123, Name: "Alice Smith", Email: "alice@new.com" }
A == B // true, same identity despite different attributes
Attribute-based equality (Value Objects):
Email A: "alice@example.com"
Email B: "alice@example.com"
A == B // true, same values
Contrast with Value Objects
| Aspect | Entities | Value Objects |
|---|---|---|
| Identity | Has unique ID | No identity |
| Equality | By ID | By attributes |
| Mutability | Mutable | Immutable |
| Lifecycle | Tracked | Replaceable |
Implementation Guidelines
Always Have an ID
- Use strongly-typed IDs (UserId, OrderId)
- Never use primitive types directly
- ID should be immutable
Implement Equality by ID
- Override
Equals()andGetHashCode() - Compare based on ID only
- Ignore attribute differences
Encapsulate State Changes
- Use methods to change state, not public setters
- Validate state transitions
- Emit domain events for significant changes
Control Creation
- Use factory methods or constructors
- Ensure valid initial state
- Generate or assign ID at creation
Common Patterns
Entity Base Class
public abstract class Entity<TId>
{
public TId Id { get; protected set; }
public override bool Equals(object obj)
{
if (obj is Entity<TId> other)
return Id.Equals(other.Id);
return false;
}
public override int GetHashCode() => Id.GetHashCode();
}Rich Entity Behavior
// Order is an aggregate root, so it inherits both identity (Entity<TId>)
// and event collection (AggregateRoot) — see [[domain-events|Domain Events]].
public class Order : AggregateRoot<OrderId>
{
private List<OrderLine> _lines = new();
public OrderStatus Status { get; private set; }
public void AddItem(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot modify submitted order");
_lines.Add(new OrderLine(product, quantity));
}
public void Submit()
{
if (!_lines.Any())
throw new InvalidOperationException("Cannot submit empty order");
Status = OrderStatus.Submitted;
// Recorded on the aggregate and dispatched after a successful
// save — see [[domain-events|Domain Events]] for the collecting pattern.
AddDomainEvent(new OrderSubmitted(Id));
}
}Related Concepts
- Value Objects - Objects without identity
- Aggregates - Clusters of entities and value objects
- Domain Events - Notifications of entity state changes
- Repositories - Persistence abstraction for entities