Value Objects are domain objects defined entirely by their attributes, with no conceptual identity.
Core Characteristics
No Identity
- Defined by attribute values, not an ID
- Two value objects with same attributes are identical
- Interchangeable if values match
Immutability
- Cannot be modified after creation
- Changes require creating a new instance
- Thread-safe by design
Equality by Value
- Equal if all attributes are equal
- Hash code based on attributes
- Structural equality, not reference equality
Examples
Email Address
public class Email : ValueObject
{
public string Value { get; }
public Email(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Email cannot be empty");
if (!IsValidEmail(value))
throw new ArgumentException("Invalid email format");
Value = value;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}Money
public class Money : ValueObject
{
public decimal Amount { get; }
public Currency Currency { get; }
public Money(decimal amount, Currency currency)
{
Amount = amount;
Currency = currency;
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(Amount + other.Amount, Currency);
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}Address
public class Address : ValueObject
{
public string Street { get; }
public string City { get; }
public string PostalCode { get; }
public string Country { get; }
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return PostalCode;
yield return Country;
}
}Benefits
Self-Validation
- Encapsulate validation logic
- Invalid states are impossible
- Type safety over primitive obsession
Domain Clarity
- Make implicit concepts explicit
Emailvsstringis clearer- Prevent primitive obsession anti-pattern
Immutability Benefits
- Thread-safe without synchronization
- Can be safely shared
- No defensive copying needed
Rich Behavior
- Methods express domain operations
money.Add(otherMoney)vsamount1 + amount2- Business rules in one place
When to Use Value Objects
Use Value Objects When:
- No conceptual identity needed
- Equality is based on attributes
- Validation rules exist
- Multiple attributes form a cohesive concept
- Immutability is desirable
Examples:
- Email, Phone Number, URL
- Money, Temperature, Distance
- Address, Date Range, Coordinates
- Colors, Dimensions, Measurements
Implementation Patterns
Base Value Object Class
public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
return false;
var other = (ValueObject)obj;
return GetEqualityComponents()
.SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x?.GetHashCode() ?? 0)
.Aggregate((x, y) => x ^ y);
}
}Wrapper Types (Tiny Types)
From Domain Driven Design:
F# example:
type EmailAddress = EmailAddress of string
type PhoneNumber = PhoneNumber of string
type CustomerId = CustomerId of integerC# example:
public readonly struct Name
{
public Name(string value)
{
_value = value;
}
private readonly string _value;
public string Value => _value;
}Value Object Operations
Side-Effect Free Functions
// Good: Returns new instance
public Money Add(Money other)
{
return new Money(Amount + other.Amount, Currency);
}
// Bad: Mutates state
public void Add(Money other)
{
this.Amount += other.Amount; // WRONG!
}Intentional Methods
public class DateRange : ValueObject
{
public DateTime Start { get; }
public DateTime End { get; }
public bool Overlaps(DateRange other)
{
return Start < other.End && End > other.Start;
}
public int DurationInDays()
{
return (End - Start).Days;
}
}Contrast with Entities
| Aspect | Value Objects | Entities |
|---|---|---|
| Identity | No identity | Unique ID |
| Equality | By attributes | By ID |
| Mutability | Immutable | Mutable |
| Lifecycle | Replaceable | Tracked |
| Usage | Describe things | Track things |
Common Value Objects
Primitives Wrapped
- EmailAddress, PhoneNumber, URL
- CustomerId, OrderId (when used as property)
Measurements
- Money, Distance, Weight, Temperature
- Duration, TimeRange, DateRange
Descriptive
- Address, FullName, Coordinate
- Color, Dimension, Size
Business Concepts
- TaxRate, Discount, Score
- Rating, Priority, Status (enum alternative)
Anti-Patterns to Avoid
Primitive Obsession
// Bad
public class User
{
public string Email { get; set; } // Just a string
}
// Good
public class User
{
public Email Email { get; set; } // Value Object
}Mutable Value Objects
// Bad
public class Money
{
public decimal Amount { get; set; } // Mutable!
}
// Good
public class Money
{
public decimal Amount { get; } // Immutable
}Related Concepts
- Entities - Objects with identity
- Aggregates - Can contain value objects
- Domain Driven Design - Overall pattern context