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

AspectEntitiesValue Objects
IdentityHas unique IDNo identity
EqualityBy IDBy attributes
MutabilityMutableImmutable
LifecycleTrackedReplaceable

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() and GetHashCode()
  • 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));
    }
}

References