Immutability#
flowchart LR
Original([Original v1])
Operation[mutating op]
V2([New value v2])
Original --> Operation --> V2
Original -.unchanged.-> Original
classDef p fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef s fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
class Original,V2 p;
class Operation s;
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class Original,Operation,V2 service;
An immutable object never changes after construction. To "update" it, you produce a new instance. Trade allocations for safety: no aliasing, no synchronisation, no defensive copies.
Mutable vs immutable#
classDiagram
class MutablePoint {
-x: int
-y: int
+setX(int)
+setY(int)
}
class ImmutablePoint {
+x: int
+y: int
+ImmutablePoint(x, y)
+withX(int) ImmutablePoint
+withY(int) ImmutablePoint
}
ImmutablePoint returns a new instance on every "change". Holders of the old one are unaffected.
How to make a class immutable#
- All fields
final. - No setters.
- Don't expose mutable internals (defensive copy on construction + getters).
- Don't
this-leak during construction. - Make the class itself
final(or seal it) so subclasses can't add mutable state.
Java records (since 14) and Kotlin data class val ... give this for free.
Why it matters for concurrency#
sequenceDiagram
participant T1 as Thread 1
participant T2 as Thread 2
participant V as Immutable Value
T1->>V: read
T2->>V: read
Note over T1,T2: no lock needed, value never changes
No race conditions on an object that can't be mutated. This is the easiest concurrency strategy that exists.
Persistent data structures#
For collections, naive copy-on-write is O(N). Persistent / functional data structures share structure under the hood - operations are O(log N) or O(1):
| Structure | Used by |
|---|---|
| Persistent vector (32-way trie) | Clojure, Scala, Immutable.js |
| HAMT (hash array mapped trie) | Clojure, Scala, .NET ImmutableDictionary |
| Persistent map / set | functional langs everywhere |
Where immutability shows up in this site#
- CRDTs - each replica works with immutable causal histories.
- Event sourcing - events are immutable; state is a projection.
- Block & event logs - Kafka, WAL, blockchain - append-only by design.
- Configuration service / Feature flags - config snapshots passed around the codebase as immutable values.
When not to be immutable#
- Tight inner loops on huge arrays in performance-critical code (allocation pressure).
- Hot data structures with high update rates (counters, caches).
- I/O buffers that must be reused.
Even then, you can localise mutation behind an immutable interface (mutable builder → build → freeze).
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Pub/Sub & message brokers | topics, consumer groups, delivery semantics | pub-sub-pattern |
HLD |
Leader/follower replication | sync/semi-sync/async replication, failover | replication-leader-follower |
HLD |
LSM vs B-Tree engines | WAL, memtable, SSTables, compaction | storage-engines-lsm-btree |
HLD |
Event sourcing + CQRS | commands -> events; separate read model | event-sourcing-cqrs |
LLD |
Data structures & complexity | Big-O, common DS, latency numbers | data-structures-complexity |
LLD |
Creational patterns | Singleton, Factory, Builder, Prototype | creational-patterns |
LLD |
Behavioural patterns | Strategy, Observer, State, Command, Chain | behavioral-patterns |
LLD |
Threading & deadlocks | thread states, Coffman, lock ordering | threading-and-deadlocks |
LLD |
Immutability | immutable types, persistent collections | immutability |
Quick reference#
Heuristics#
- Default to immutable. Make a class mutable only when there's a reason.
- Value objects (Money, Date, IP, URL) almost always immutable.
- Entity objects (User, Account) often need controlled mutation.
- Containers passed across thread / module boundaries → immutable.
Costs#
- Allocations + GC pressure.
- Awkward when you need to update one field of a 30-field object - language sugar helps (
copy(x = ...)in Kotlin, records'withers).
Language support#
- Java records,
recordkeyword. - Kotlin
data class val. - Python
frozen=Truedataclass;tuple,frozenset. - C# records,
readonly struct,ImmutableList. - Rust default-immutable;
mutis opt-in. - Clojure / Haskell - immutable from the ground up.
Refs#
- Effective Java - Item 17 (minimize mutability).
- "Out of the Tar Pit" - Moseley & Marks, 2006.
- Rich Hickey: "The Value of Values".
FAQ#
What is an immutable object?#
An immutable object cannot be changed after construction. Any update produces a new instance rather than mutating the original, which keeps shared state safe.
Why use immutability in concurrent code?#
Immutable values cannot have race conditions because no thread can mutate them. You can share references freely without locks or defensive copies.
Does immutability hurt performance?#
It can add allocations, but persistent data structures share most of their content via structural sharing. The simpler reasoning often more than offsets the cost.
What is the difference between final and immutable in Java?#
Final means the reference cannot be reassigned. Immutable means the object's state cannot change. A final field can still point to a mutable object.
How do I create an immutable class?#
Mark fields private and read-only, do not expose setters, copy mutable inputs in the constructor, and return defensive copies of any internal collections.
Related Topics#
- Concurrency Primitives: immutability eliminates the need for many locks and synchronization constructs
- SOLID Principles: immutable value objects reinforce SRP and reduce hidden coupling
Further reading#
Curated, high-credibility sources for going deeper on this topic.