Value Object

Value Object

"An object that describes some characteristic or attribute but carries no concept of identity." — Evans, Domain-Driven Design, 2003

Intent

A Value Object is a domain concept defined entirely by its attributes. Two Value Objects are equal if and only if all their attribute values are equal — there is no separate identity field, no surrogate key, no lifecycle to track. Money(10, "USD") equals any other Money(10, "USD") regardless of which instance created them.

Value Objects are immutable. There are no setters. Any operation that would "mutate" a Value Object instead returns a new instance (copy-on-mutation). This makes Value Objects safe to share, thread-safe by default, and trivially testable — a Value Object with the same input always produces the same result.

Value Objects eliminate primitive obsession: instead of passing raw number or String for a price, a postal code, or an email address, the domain model uses Money, PostalCode, or EmailAddress — types that carry their own validation invariants and make illegal states unrepresentable.

When NOT to Use

  • The object has a meaningful lifecycle or identity across time — use Entity instead. An Order is an Entity: Order #12345 retains its identity even when its status changes from pending to confirmed.
  • The object is large and mutated very frequently, making copy-on-mutation cost prohibitive (e.g., a large immutable collection updated millions of times per second).
  • Equality-by-value semantics are not meaningful for the concept: two Employee records with the same name are not the same employee.
  • The surrounding persistence layer (legacy ORM) cannot accommodate immutable types without significant workarounds — consider whether the friction is worth the correctness benefit.

When to Use

  • Modelling domain concepts defined entirely by their attributes: Money, EmailAddress, DateRange, Quantity, Colour, PostalCode, GeoCoordinate.
  • Eliminating primitive obsession — replacing raw number, string, or Date parameters with self-validating types that reject invalid values at construction time.
  • Enforcing invariants at construction time so that an invalid Value Object can never exist in the system (e.g., Money cannot have a negative amount; EmailAddress cannot be malformed).
  • Any concept where "same value = same thing" is the natural language of the domain.

How It Works

Three defining characteristics govern Value Objects:

  1. Equality by value — two instances are equal when all their fields are equal; equality is implemented by comparing properties, not object references (=== / Object.is comparisons are wrong for Value Objects).
  2. Immutability — no setters. Once constructed, properties never change. Operations that would mutate state return a new instance via a copy constructor or factory method (add(), withCurrency(), toUpperCase()). Immutability is enforced at the language level where possible (Object.freeze in TypeScript, record in Java, final fields with no setters in older Java).
  3. Self-validating constructors — the constructor (or a create() factory) checks all invariants and throws on violation. After construction, a valid Value Object is guaranteed to remain valid forever because it cannot be mutated.

Value Objects have no repository (they are never persisted independently), no separate identity column, and no surrogate key. In relational databases they are stored inline inside the owning Entity's row (JPA @Embeddable).

Class Diagram

classDiagram
    class ValueObject {
        <<immutable>>
        #props : ReadonlyProps
        +equals(other : ValueObject) bool
        +toString() string
    }
    class Money {
        <<immutable>>
        -amount : number
        -currency : string
        +add(other : Money) Money
        +equals(other : Money) bool
    }
    class EmailAddress {
        <<immutable>>
        -value : string
        +create(raw : string)$ EmailAddress
        +equals(other : EmailAddress) bool
    }
    class DateRange {
        <<immutable>>
        -start : Date
        -end : Date
        +contains(date : Date) bool
        +overlaps(other : DateRange) bool
    }

    ValueObject <|-- Money
    ValueObject <|-- EmailAddress
    ValueObject <|-- DateRange

    note for ValueObject "No identity (no ID field)\nEquality by value\nImmutable after construction\nSelf-validating constructor"

TypeScript Example

// Value Object — TypeScript
// Source: Evans, Domain-Driven Design, 2003 + khalilstemmler.com
interface ValueObjectProps { [index: string]: unknown }
 
abstract class ValueObject<T extends ValueObjectProps> {
  protected readonly props: T;
  constructor(props: T) {
    this.props = Object.freeze({ ...props }); // immutability enforced at construction
  }
  equals(vo?: ValueObject<T>): boolean {
    if (!vo) return false;
    return JSON.stringify(this.props) === JSON.stringify(vo.props);
  }
}
 
class Money extends ValueObject<{ amount: number; currency: string }> {
  get amount() { return this.props.amount; }
  get currency() { return this.props.currency; }
  add(other: Money): Money {
    if (other.currency !== this.currency) throw new Error("Currency mismatch");
    return new Money({ amount: this.amount + other.amount, currency: this.currency });
  }
  static create(amount: number, currency: string) { return new Money({ amount, currency }); }
}

Java Example

// Value Object — Java (record — immutable by default)
// Source: Evans, Domain-Driven Design, 2003
// Records provide equals/hashCode/toString by value automatically
// Note: use @Embeddable on the record for JPA persistence contexts
public record Money(BigDecimal amount, String currency) {
    public Money {  // compact constructor for validation
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
        if (amount.compareTo(BigDecimal.ZERO) < 0) throw new IllegalArgumentException("Negative amount");
    }
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) throw new IllegalArgumentException("Currency mismatch");
        return new Money(this.amount.add(other.amount), this.currency);
    }
}

Lineage Backward

Prototype-Pattern — Prototype's copy semantics are the GoF foundation of Value Object immutable copy-on-mutation. Both patterns avoid sharing mutable state by producing new instances rather than modifying existing ones. Where Prototype copies entire object graphs, Value Object applies the same principle at the smallest domain granularity.

Lineage Forward

Aggregate — Aggregates are composed of Entities and Value Objects. The Aggregate root enforces invariants over its contained Value Objects. A Money Value Object, for example, lives inside an Order Aggregate and is never accessed independently.

PatternRelationship
EntityIdentity vs value equality. Entity has a lifecycle and an identity that persists through state changes; Value Object has neither. When in doubt: "does it matter which instance this is?" Yes → Entity. No → Value Object.
Prototype-PatternCopy semantics ancestor. Prototype introduced the pattern of creating new instances by copying existing ones; Value Object applies this as its mutation model.
AggregateValue Objects are Aggregate building blocks. The Aggregate root owns Value Objects; they are never accessed or persisted independently.
  • Clean-Architecture — Value Objects are Entity building blocks and live in the Entities ring, the innermost layer; they carry zero framework or infrastructure dependencies.
  • Onion-Architecture — Value Objects belong in the Domain Model ring (innermost) of Onion Architecture, alongside entities and repository interfaces, with no dependencies on any outer ring.

Sources

  • Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
  • Fowler, Martin. bliki/ValueObject — https://martinfowler.com/bliki/ValueObject.html
  • Stemmler, Khalil. "Value Objects — DDD w/ TypeScript" — khalilstemmler.com