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
  • Email vs string is 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) vs amount1 + 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 integer

C# 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

AspectValue ObjectsEntities
IdentityNo identityUnique ID
EqualityBy attributesBy ID
MutabilityImmutableMutable
LifecycleReplaceableTracked
UsageDescribe thingsTrack 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
}

References