Why should Value Objects be immutable, and what does 'side-effect-free behavior' mean in practice — for example, should a method like `Money.add(Money other)` mutate the receiver or return a new instance?
answer
- VO immutability prevents aliasing bugs
- operations return new instances, never mutate self
- final fields / no setters / constructor validation
- mutable VO as hash key = lost entries after mutation
basics
~20 sValue Objects should never change after creation. Any 'change' should produce a brand-new object instead of altering the original. So Money.add() should return a new Money with the summed amount, not modify the original — that way nothing else holding a reference to the original gets silently surprised.
solid answer
~50 sValue Objects are made immutable because their entire identity IS their data — if you mutate one in place, you break the guarantee that two Value Objects with equal data are truly interchangeable at every point in time, and you introduce aliasing bugs: any other code holding a reference to that instance sees its value change out from under it. 'Side-effect-free' means operations on a Value Object never modify internal state and never modify shared/external state; they compute and return a new Value Object instead. `Money.add(Money other)` should be implemented as `return new Money(this.amount.add(other.amount), this.currency)`, leaving both operands untouched. This mirrors how primitives like BigDecimal or immutable String work — `a + b` never changes `a`. The immediate benefits are thread-safety without locking, safe sharing/caching, predictable equals()/hashCode() (a hash key that never changes after insertion), and easier reasoning/testing since a Value Object can never 'go stale' unexpectedly.
go deeper
Should know that a Value Object shouldn't have setters and that operations like add() should return a new object rather than change the existing one.
Should be able to implement an immutable VO correctly (final fields, constructor validation, no mutating methods) and explain the aliasing bug immutability prevents.
Should know the shallow-copy pitfall with generated copy()/with-style methods, the mutable-collection-field back door, and be able to spot a mutable VO being used unsafely as a cache/map key in a design review.
Should weigh immutability's allocation cost against correctness for a specific performance-critical scenario and design an appropriate boundary (e.g., a bounded mutable builder that produces an immutable VO) rather than blanket-applying or blanket-rejecting the rule.
## Why immutability is the load-bearing property Immutability for Value Objects is not a style preference — it's the property that makes structural equality trustworthy over time. If two Value Objects are considered equal because their data matches, that equality is only meaningful as long as the data can't silently change underneath a reference someone else is holding. ## The aliasing bug it prevents Concretely: suppose `Money price = new Money(10, "USD")` is stored as a field on a `Product`, and elsewhere the same Money instance (not a copy) is also stored in a `PriceHistory` list for auditing. If Money were mutable and someone later called `price.setAmount(15)` to 'apply a discount', the PriceHistory entry would silently change too, because both references point at the same mutable object — the audit trail is corrupted without anyone touching it directly. This is **aliasing**: two variables referring to the same object in memory, and it's the single biggest practical argument for Value Object immutability. ## What side-effect-free behavior means The fix DDD prescribes: any operation that looks like a 'change' — applying a discount, adding two amounts, converting a currency — must be implemented as a pure function that returns a brand-new instance, leaving the original(s) untouched. This is **side-effect-free behavior**: calling a method on a Value Object never has any observable effect on its own state or on anything else in the system; the only output is the return value. Mechanically, this looks like: `public Money add(Money other) { requireSameCurrency(other); return new Money(this.amount.add(other.amount), this.currency); }` — this.amount itself is never reassigned; a new Money wraps a new sum. This typically means: - every field is final, - there are no setters, - and the constructor performs full validation so an instance can never exist in an invalid state. In languages with data classes, a `copy()` method is the idiomatic way to produce 'a changed version', which is the same 'return a new instance' pattern under a different name. ## Three reasons it is a rule, not a suggestion Why is this a rule rather than a suggestion? 1. **Concurrency** is the sharpest reason: an immutable object can be freely shared across threads with zero synchronization, because there is no mutable state to race on — this is why `String`, `Integer`, `LocalDate`, and `BigDecimal` are all immutable in Java, and why immutability is a first-class recommendation far beyond DDD specifically. 2. The second reason is **equality/hash-collection safety**: if a Value Object is used as a `HashMap` key or stored in a `HashSet`, mutating it after insertion changes its hashCode and 'loses' it in the collection the same way a mutating Entity id does — except for a Value Object this is even more insidious because there's no separate stable identity field to fall back on; the entire object IS the key. 3. The third reason is **simple reasoning**: a function that takes an immutable Value Object as a parameter can be certain the caller's object won't change while the function runs, eliminating a whole class of 'who mutated this and when' bugs. ## The trade-off The trade-off is allocation cost: every 'change' to a Value Object allocates a new object rather than mutating in place, which is more garbage-collector pressure in extremely hot paths. In the overwhelming majority of business applications this cost is negligible compared to I/O, database, and network costs, and modern generational garbage collectors handle short-lived small objects cheaply — trading a small amount of allocation for eliminating an entire bug category is almost always the right call; teams only reach for mutable value-like structures in genuinely performance-critical numeric code, and even then usually confine the mutation to a tightly scoped, clearly-documented builder or accumulator converted to an immutable Value Object at the boundary. ## A real production failure mode A real production failure mode: a caching layer memoizes results keyed by a `DateRange` Value Object. A developer adds a `shiftBy(int days)` method that mutates the DateRange's internal start/end fields in place and returns `this` for chaining, instead of returning a new instance. Months later, a scheduling feature reuses a cached DateRange instance across two different report requests and calls `shiftBy()` on it for the second report — the first report's cached entry, which shares that same instance as its map key, silently starts returning wrong data because the key object mutated after being inserted into the cache's internal `HashMap`. The bug is intermittent, hard to reproduce, and traces back entirely to one method breaking the immutability contract.
- Kotlin's data class copy() seems to make 'immutable with changes' easy — are there any pitfalls?copy() only does a shallow copy — if a field is itself a mutable reference type (e.g., a mutable List), copy() shares that same inner mutable object between the original and the copy, so mutating the shared list through either instance breaks both. To be truly safe, every field of a Value Object should itself be immutable, all the way down.
- Does immutability mean a Value Object can never contain a collection?No, but the collection field must be exposed and stored immutably — e.g., wrap it in an unmodifiable/persistent collection type at construction and never expose the raw mutable collection through a getter, otherwise a caller can mutate 'through the back door' even though there's no setter.
- How does side-effect-free Value Object behavior interact with validation — where should invariant checks like 'amount must be non-negative' live?They belong in the constructor (or a static factory method), so an instance can never exist in an invalid state in the first place. Since the object never mutates afterward, validating once at construction is sufficient; there's no need to re-validate on every read.
A paper receipt vs. a whiteboard tally: a receipt (immutable Value Object) is printed once and handing someone a copy never changes the original; a whiteboard tally (mutable) can be erased and rewritten, so if two people are both looking at the same whiteboard, one person's edit silently changes what the other sees.
saying these in an interview costs you the question
- Implements Money.add() by mutating this.amount and returning this
- Adds a public setter to a class the candidate also calls a Value Object
- Doesn't recognize aliasing as the reason immutability matters, treats it as arbitrary style
- Exposes a raw mutable List/Map field via a getter on an otherwise 'immutable' Value Object
- Thinks immutability is only about thread-safety and misses the hash-key/aliasing angle