The idea
What it is
Think about a banknote with the number $100 printed on it. You can't edit a $100 bill into a $50 bill — the value is fixed the moment it's made. If you want $50, you hand over the $100 and get a different note back. An immutable object works exactly like that: once it's built, its data never changes. Any "change" hands you a brand-new object and leaves the original alone.
Most bugs in shared state come from something quietly editing an object that other code is still relying on. Immutability removes that whole category of bug by making the edit impossible. A date, a coordinate, a sum of money — these are things whose identity is their value, and they're far easier to reason about when they simply can't be mutated out from under you.
The one sentence to remember
Don't change the object — replace it. An immutable value never mutates; operations like plus or withAmount return a new object, so anyone still holding the old one is safe.
Mechanics
How it works
What makes an object immutable
An object is immutable when there is no legal way to change its data after it's constructed. In practice that means three things working together:
- Set every field once, at construction. Mark fields
final(Java),readonly(TypeScript), orconst(C++) so the compiler refuses any later write. - Expose no setters. Provide getters if you like, but nothing that writes. The only moment the data is set is in the constructor.
- Deep-copy on the way in and out. If the constructor receives a mutable thing (a list, an array, a date), store a copy, not the caller's object. If a getter returns a mutable thing, return a copy of that too.
The aliasing bug — one mutation, two surprises
Here's the trap immutability saves you from. When you assign one object to two variables, you don't get two objects — you get two references to the same object. Mutate it through one, and the other sees the change too, often somewhere far away in the code that never asked for it.
// MUTABLE Money — has a setter
const aliasA = new MutableMoney(100, "USD");
const aliasB = aliasA; // NOT a copy — same object, two names
aliasA.setAmount(0); // mutate through one reference...
console.log(aliasB.amount); // 0 ← aliasB changed too! spooky action
// With an IMMUTABLE Money there is no setAmount at all,
// so this bug simply cannot be written.This is aliasing. It's not a typo or a logic slip — the code reads perfectly. The damage is that aliasB was silently corrupted by something it had no relationship with. Defensive copying (storing and returning copies) is one fix; making the object immutable is the stronger one, because then there's nothing to mutate in the first place.
How an immutable object "changes"
If you can't edit the object, how do you get a different value? You build a new one. A method like plus(amount) doesn't touch this — it constructs and returns a fresh object with the new value, leaving the original exactly as it was:
class Money {
constructor(readonly amount: number, readonly currency: string) {}
plus(extra: number): Money {
return new Money(this.amount + extra, this.currency); // NEW object
}
withAmount(amount: number): Money {
return new Money(amount, this.currency); // NEW object
}
}
const a = new Money(100, "USD");
const b = a.plus(50); // b is Money(150,"USD"); a is STILL Money(100,"USD")Notice the shape: every "mutator" is really a factory that returns a new instance. The original is read-only and survives untouched, so any other reference pointing at it is never surprised.
Value objects: equality by value, no identity
A value object is a small immutable object whose identity is its value. Two Money(100, "USD") objects are completely interchangeable — it makes no sense to ask which $100 you have, only whether it's $100. So value objects compare by value: Money(100,"USD").equals(Money(100,"USD")) is true even though they're separate instances in memory.
Contrast that with an entity — a User, an Order, a BankAccount. Two users named "Alice" with the same balance are not the same user; each has its own identity (an id) that persists even as its data changes. Entities are compared by id; value objects are compared by their fields. Money, dates, coordinates, colors, and quantities are classic value objects.
Shallow vs. deep immutability
A final/readonly field only freezes the reference, not what it points at. An immutable object holding a final List whose contents can still be added to is not truly immutable — callers can mutate the list behind your back. Real immutability is deep: copy the list in, store an unmodifiable view, and never hand out the live collection. In JavaScript, Object.freeze() is likewise shallow — it freezes the top level only.
Thread-safety, for free
Because an immutable object never changes, multiple threads can read it at the same time with zero locks, zero synchronization, and no chance of seeing a half-updated state. There's no write to race against. This is one of the biggest practical wins of immutability: shared, immutable data is automatically safe to pass anywhere, including across threads.
What each language gives you
- Java —
finalfields andrecordtypes (which are immutable and get value equality for free). - Python —
@dataclass(frozen=True)raises on any attribute write;tupleis the built-in immutable sequence. - C++ —
constmembers and methods markedconst; "mutators" return a new value object. - TypeScript —
readonlyfields (compile-time) plusObject.freeze()for a runtime, shallow freeze.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
Two references, aliasA and aliasB, both point at one Money(100, "USD") box. In Mutable mode, pressing aliasA.setAmount(0) corrupts the shared box and aliasB unexpectedly reads 0 too — the shield flashes red. Switch to Immutable mode: aliasA.plus(...) builds a new box and swings only aliasA's arrow to it; aliasB stays on the untouched original and the change flashes green.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Trigger the aliasing bug
Start in Mutable mode. Both aliasA and aliasB point at one Money(100) box. Press aliasA.setAmount(0) — the shared box flashes red and the log warns aliasB unexpectedly changed!. One mutation corrupted a reference that never asked for it.
Flip to Immutable and repeat the action
Toggle the mode to Immutable, then press aliasA.plus(...). A new box appears, aliasA's arrow swings over to it, and aliasB stays on the original 100 box — flashing green. Same intent, but the original is untouched: aliasB safe — original unchanged.
Prove value equality
Press aliasA.equals(aliasB) (or the equality check). Even with two separate Money(100, "USD") instances, the result is true — value objects are equal when their fields match, identity doesn't matter.
In practice
When to use it — and what trips people up
When to reach for an immutable value object
Use immutability for any small concept whose meaning is its value — money, dates, ranges, coordinates, colors, quantities, identifiers. Reach for it whenever an object will be shared across the codebase, used as a map or set key, passed between threads, or cached: in all of those cases the guarantee that it can't change quietly is worth far more than the cost of an occasional new allocation. Keep mutability for things that genuinely model an evolving lifecycle — an Order being filled, a Game in progress — and even then, build them out of immutable value objects.
Watch the hidden mutable field
An object isn't immutable just because its fields are final or readonly. If one of those fields is a mutable list, map, or date, copy it on the way in and never hand out the live reference — otherwise a caller mutates your "immutable" object through the back door.
What it gives you
- Safe to share freely — pass it anywhere; no caller can change it under you.
- Whole classes of aliasing bugs vanish: there's no setter to corrupt shared state.
- Thread-safe by default — read from any number of threads with no locks.
- Great as map/set keys and easy to cache, because the value (and its hash) never changes.
Common mistakes
- Every "change" allocates a new object, which can mean GC churn in hot loops.
- Awkward for large state that updates often — copying a big structure per edit is wasteful.
- More ceremony than a plain mutable field: constructors, copy-in/copy-out, factory-style mutators.
- Easy to get partially immutable — a final reference to a mutable list isn't truly immutable.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// readonly fields, no setters; "mutators" return a NEW Money.
class Money {
constructor(
readonly amount: number,
readonly currency: string,
) {
Object.freeze(this); // shallow runtime freeze; fields are primitives here
}
plus(extra: number): Money {
return new Money(this.amount + extra, this.currency); // new object
}
withAmount(amount: number): Money {
return new Money(amount, this.currency); // new object
}
// value equality — equal when the fields are equal, not the references
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
const a = new Money(100, "USD");
const b = a.plus(50); // b = 150 USD; a is STILL 100 USD
const c = new Money(100, "USD");
a.equals(c); // true — same value, different instances
// a.amount = 0; // error: Cannot assign to 'amount' (readonly)References & further reading
4 sources- Articleen.wikipedia.org
Wikipedia — Immutable object
A broad cross-language overview of what immutability is and why it helps.
- Docsdocs.oracle.com
Oracle — A Strategy for Defining Immutable Objects
The canonical checklist for making a Java class immutable, including defensive copying.
- Docsdeveloper.mozilla.org
MDN — Object.freeze()
How JavaScript's runtime freeze works — and the crucial detail that it's shallow.
- Articlemartinfowler.com
Martin Fowler — ValueObject
The definition of a value object and why equality by value (not identity) matters.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Nothing is scored or saved.
question 01 / 05
What is the defining property of an immutable object?
question 02 / 05
aliasB = aliasA; with a mutable Money, then aliasA.setAmount(0). What does aliasB.amount read?
question 03 / 05
On an immutable Money(100, "USD"), what does m.plus(50) do?
question 04 / 05
How does a value object differ from an entity?
question 05 / 05
An object has only final fields, but one of them is a List you can still .add() to. Is the object truly immutable?
0/5 answered