Value Object
DDD Value Object — objects defined by attributes, not identity; equality by value
Tags
Overview
Purpose
DDD Value Object — objects defined by attributes, not identity; equality by value
Context
Domain models where many concepts describe measurements, quantities, descriptions, or computed characteristics rather than uniquely identifiable things. The system needs to compare, combine, and pass around these concepts without tracking which specific instance is which. Creation cost is low and instances are numerous. The modeling language or framework supports either value semantics natively (structs, records) or by convention (immutable classes with equals/hashCode overrides).
Problem
How to model domain concepts that have no meaningful identity — where two
instances with the same attributes should be treated as equal and
interchangeable — without the overhead and bugs of identity management?
Forces:
(1) Many domain concepts are descriptive, not identifiable — tracking
identity for them adds complexity with no domain benefit
(2) Shared mutable objects cause aliasing bugs when one holder mutates
state that another holder depends on
(3) Equality semantics must be attribute-based: Money(100, USD) must
equal Money(100, USD) regardless of which instance is which
(4) Performance requires that value-bearing objects be freely copyable,
cacheable, and usable as map keys or set members
(5) Domain operations on values (add, multiply, combine) should produce
new values rather than mutating existing ones to preserve referential
transparency
Solution
Model the concept as a Value Object: an object whose identity is defined entirely by its attribute values. Make the object immutable — all attributes are set at construction and never changed afterward. Implement equality based on attribute comparison, not reference identity. When a different value is needed, create a new instance rather than modifying the existing one. Domain operations return new Value Objects, making all behavior side-effect-free.
The design principle: if you cannot meaningfully ask "which one?" then the concept is a Value Object. If two instances with identical attributes are interchangeable with no loss of meaning, model it as a value.
Consequences
Benefits:
- Eliminates aliasing bugs: immutability means sharing is always safe
- Simplifies equality: attribute comparison is deterministic and domain-meaningful
- Enables optimization: immutable objects can be freely cached, interned, and
used as hash keys without defensive copying
- Side-effect-free behavior makes reasoning about domain logic easier and
enables safe parallelism
- Reduces identity management overhead: no need for IDs, repositories, or
lifecycle tracking for descriptive concepts
- Composability: Value Objects combine naturally (Money + Money = Money)
Liabilities:
- Requires discipline: developers must enforce immutability by convention in
languages without native value types (Java, JavaScript, Python)
- Object creation overhead: every modification creates a new instance, which
may cause GC pressure in high-frequency scenarios
- Not suitable for concepts that must be tracked through time or distinguished
when attributes are identical
- Nested Value Objects increase construction complexity (deep builders)
- Schema evolution is harder: adding an attribute changes the equality contract
and may break existing comparisons or serialized data
Collaborations
Entity holds ValueObject instances as attributes describing its characteristics
(e.g., a Customer entity holds an Address value object). When the Entity needs
a different value, it replaces the entire ValueObject with a new instance rather
than mutating the existing one. Factory creates ValueObjects when construction
requires validation or invariant enforcement — for simple Value Objects, direct
construction suffices. ValueObjects may compose other ValueObjects (e.g., Money
composes Currency and Amount, both themselves value-typed).
Structure
Participants:
- ValueObject: Immutable object, equality by attributes, side-effect-free operations
- Entity: Contains Value Objects as descriptive attributes
- Factory: Creates complex Value Objects with validated invariants
Relationships:
- Entity contains ValueObject (composition, value semantics)
- Factory creates ValueObject (creation)
- ValueObject may compose other ValueObjects (nesting)
Structural invariants:
- All ValueObject attributes are set at construction (immutability)
- equals() and hashCode() are based on ALL attributes
- No setter methods exist on ValueObject
- Domain operations return new instances, never modify this
Implementation
Design guidelines:
1. Make all fields final/readonly at construction
- Go: unexported fields + constructor function
- TypeScript: readonly properties + constructor
- Java: final fields + no setters
2. Implement equality by attribute comparison
- Go: implement Equal(other) bool comparing all fields
- TypeScript/Java: override equals() comparing all attributes
- Override hashCode() to match equals() contract
3. Return new instances from all operations
- money.Add(other) returns new Money, never modifies receiver
- Use "with" methods for single-field changes: address.WithCity("new")
4. Consider making Value Objects serializable
- They are natural candidates for JSON/YAML serialization
- Deserialization reconstructs via constructor (validates invariants)
5. In database schemas, Value Objects typically embed into the owning
Entity's table as columns (no separate table, no foreign key).
This mirrors the "no identity" principle at the persistence layer.
6. In SBX .framework, the entity.yml `attributes` map implements
value-object semantics. Each attribute is value-bound to its
parent entity, has no independent lifecycle, and participates
in the entity's equality-by-value characteristics.
Example
Consider an e-commerce system that processes orders in multiple currencies. A developer models Money as a mutable class with an amount field and a currency field. Two separate Order objects both reference the "same" Money instance for their price. When one order applies a discount by mutating the amount, the other order's price silently changes. Debugging reveals that shared mutable state has corrupted the data — the system cannot distinguish between "the same money" and "money of equal value." The root cause is that Money was modeled as an Entity (shared by reference, mutable) when it should have been modeled as a value (shared by copy, immutable, equality by attributes).
Resolution
Applying the Value Object pattern to the e-commerce Money problem: Money is
redesigned as an immutable value with amount and currency set at construction.
equals() compares both fields. The add() method returns a new Money instance.
Now two orders can safely hold Money(100, USD) without aliasing — each has its
own immutable copy. Applying a discount to one order creates a new Money(90, USD)
assigned to that order, leaving the other order's price untouched. The aliasing
bug is structurally impossible because Money has no mutable state to corrupt.
Known Uses
- Money pattern (Fowler PoEAA, Evans DDD) — amount + currency as immutable value, used in virtually every financial system
- java.time API (JSR-310) — LocalDate, LocalTime, Duration, Instant are all immutable Value Objects replacing the mutable java.util.Date
- Go standard library — time.Time, net.IP, math/big.Int are value-typed with copy semantics and equality by value
- Joda-Money / javax.money (JSR-354) — Money and CurrencyUnit as immutable values in Java financial applications
- Address in virtually every DDD reference implementation (Evans, Vernon, Millett) — street/city/zip as value, no identity
See Also
- entity-pattern: Entity and Value Object are mutually exclusive classifications. An object is either tracked by identity (Entity) or defined by attributes (Value Object). The choice is the most fundamental modeling decision in DDD.
- aggregate: Aggregates compose Value Objects as internal components. Value Objects within an aggregate boundary share the aggregate's transactional scope and are deleted with the root.
- typed-map-composition: SBX entity.yml attributes map applies value-object semantics through typed-map-composition — each attribute entry conforms to the attribute protocol with value-bound lifecycle.