What is the difference between Entities and Value Objects in DDD?
Quick Answer
An Entity has a unique identity that persists through changes — two entities with the same attributes but different IDs are different, and equality is by identity. A Value Object has no identity; it's defined entirely by its attribute values, is immutable, and equality is by value (e.g., Money, Address). Model concepts you track over time as entities and descriptive, interchangeable concepts as value objects.
Detailed Answer
Entities and Value Objects are fundamental building blocks in Domain-Driven Design, but they serve different purposes and have distinct characteristics. Understanding their differences is crucial for creating effective domain models.
Key Differences:
| Aspect | Entity | Value Object |
|---|---|---|
| Identity | Has unique identity | No identity, defined by attributes |
| Equality | Compared by identity | Compared by value |
| Mutability | Mutable | Immutable |
| Lifecycle | Long-lived, persists over time | Short-lived, can be recreated |
| Tracking | Tracked by ID | Not tracked individually |
1. Entities - Identity Matters
Entities are objects with a distinct identity that persists over time. Their identity is more important than their attributes.
public class Customer : Entity<CustomerId>
{
public CustomerId Id { get; private set; }
public string Name { get; private set; }
public string Email { get; private set; }
public DateTime CreatedAt { get; private set; }
public Customer(CustomerId id, string name, string email)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
Name = name ?? throw new ArgumentNullException(nameof(name));
Email = email ?? throw new ArgumentNullException(nameof(email));
CreatedAt = DateTime.UtcNow;
}
public void UpdateEmail(string newEmail)
{
Email = newEmail ?? throw new ArgumentNullException(nameof(newEmail));
}
public void ChangeName(string newName)
{
Name = newName ?? throw new ArgumentNullException(nameof(newName));
}
// Identity-based equality
public override bool Equals(object obj)
{
if (obj is not Customer other) return false;
return Id.Equals(other.Id);
}
public override int GetHashCode() => Id.GetHashCode();
}
// Usage
var customer1 = new Customer(new CustomerId(1), "John Doe", "john@example.com");
var customer2 = new Customer(new CustomerId(1), "Jane Smith", "jane@example.com");
// These are considered the same entity (same ID)
Console.WriteLine(customer1.Equals(customer2)); // True
Console.WriteLine(customer1 == customer2); // True (if operator overloaded)
2. Value Objects - Value Matters
Value objects are defined by their attributes rather than identity. They are immutable and compared by value.
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0) throw new ArgumentException("Amount cannot be negative");
if (string.IsNullOrWhiteSpace(currency))
throw new ArgumentException("Currency is required");
Amount = amount;
Currency = currency.ToUpperInvariant();
}
// Value-based equality
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
public static Money operator +(Money left, Money right)
{
if (left.Currency != right.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(left.Amount + right.Amount, left.Currency);
}
public static Money operator *(Money money, decimal multiplier)
{
return new Money(money.Amount * multiplier, money.Currency);
}
}
public class Address : ValueObject
{
public string Street { get; }
public string City { get; }
public string State { get; }
public string ZipCode { get; }
public string Country { get; }
public Address(string street, string city, string state, string zipCode, string country)
{
Street = street ?? throw new ArgumentNullException(nameof(street));
City = city ?? throw new ArgumentNullException(nameof(city));
State = state ?? throw new ArgumentNullException(nameof(state));
ZipCode = zipCode ?? throw new ArgumentNullException(nameof(zipCode));
Country = country ?? throw new ArgumentNullException(nameof(country));
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return State;
yield return ZipCode;
yield return Country;
}
}
// Usage
var money1 = new Money(100, "USD");
var money2 = new Money(100, "USD");
var money3 = new Money(100, "EUR");
Console.WriteLine(money1.Equals(money2)); // True - same value
Console.WriteLine(money1.Equals(money3)); // False - different currency
// Value objects can be recreated
var newMoney = new Money(200, "USD"); // Creates new instance
3. Practical Examples
Entity Example - Order:
public class Order : Entity<OrderId>
{
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public Money Total { get; private set; }
public OrderStatus Status { get; private set; }
public Address ShippingAddress { get; private set; } // Value Object
private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public Order(OrderId id, CustomerId customerId, Address shippingAddress)
{
Id = id;
CustomerId = customerId;
ShippingAddress = shippingAddress; // Value Object
Status = OrderStatus.Draft;
Total = new Money(0, "USD");
}
public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
{
// Business logic for adding items
var item = new OrderItem(productId, quantity, unitPrice);
_items.Add(item);
RecalculateTotal();
}
public void UpdateShippingAddress(Address newAddress)
{
// Can replace the entire value object
ShippingAddress = newAddress;
}
private void RecalculateTotal()
{
Total = _items.Aggregate(new Money(0, "USD"), (sum, item) => sum + item.Total);
}
}
Value Object Example - OrderItem:
public class OrderItem : ValueObject
{
public ProductId ProductId { get; }
public Quantity Quantity { get; }
public Money UnitPrice { get; }
public Money Total => UnitPrice * Quantity.Value;
public OrderItem(ProductId productId, Quantity quantity, Money unitPrice)
{
ProductId = productId ?? throw new ArgumentNullException(nameof(productId));
Quantity = quantity ?? throw new ArgumentNullException(nameof(quantity));
UnitPrice = unitPrice ?? throw new ArgumentNullException(nameof(unitPrice));
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return ProductId;
yield return Quantity;
yield return UnitPrice;
}
public OrderItem WithIncreasedQuantity(Quantity additionalQuantity)
{
// Return new instance instead of mutating
return new OrderItem(ProductId, new Quantity(Quantity.Value + additionalQuantity.Value), UnitPrice);
}
}
4. When to Use Each
Use Entities when:
- The object has a distinct identity
- You need to track the object over time
- The object can change its attributes but remains the same
- You need to reference the object from other parts of the system
// Customer is an entity - we track them by ID
public class Customer : Entity<CustomerId>
{
public CustomerId Id { get; private set; }
public string Name { get; private set; }
public string Email { get; private set; }
// Customer can change name/email but remains the same customer
public void UpdateProfile(string newName, string newEmail)
{
Name = newName;
Email = newEmail;
}
}
Use Value Objects when:
- The object is defined by its attributes
- The object is immutable
- You don't need to track individual instances
- The object represents a concept or measurement
// Money is a value object - defined by amount and currency
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
// Money is immutable - operations return new instances
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(Amount + other.Amount, Currency);
}
}
5. Common Mistakes
❌ Wrong: Treating Value Objects as Entities
// BAD: Giving identity to something that should be a value object
public class Money : Entity<MoneyId>
{
public MoneyId Id { get; set; } // Unnecessary identity
public decimal Amount { get; set; }
public string Currency { get; set; }
}
❌ Wrong: Making Entities Mutable in Wrong Ways
// BAD: Exposing setters that break encapsulation
public class Customer : Entity<CustomerId>
{
public CustomerId Id { get; set; } // Should be private set
public string Name { get; set; } // Should be private set
public string Email { get; set; } // Should be private set
}
✅ Correct: Proper Separation
// GOOD: Entity with proper encapsulation
public class Customer : Entity<CustomerId>
{
public CustomerId Id { get; private set; }
public string Name { get; private set; }
public string Email { get; private set; }
public void UpdateEmail(string newEmail)
{
// Business logic for email validation
if (string.IsNullOrWhiteSpace(newEmail))
throw new ArgumentException("Email cannot be empty");
Email = newEmail;
}
}
// GOOD: Value object that's immutable
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// Operations return new instances
public Money Add(Money other) => new Money(Amount + other.Amount, Currency);
}
Key Takeaways:
- Entities have identity and are mutable
- Value Objects have no identity and are immutable
- Entities are tracked by ID, Value Objects by value
- Entities can change attributes, Value Objects are replaced
- Entities are long-lived, Value Objects can be recreated
- Use Entities for things that have identity, Value Objects for concepts and measurements