Skip to content

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#

  1. All fields final.
  2. No setters.
  3. Don't expose mutable internals (defensive copy on construction + getters).
  4. Don't this-leak during construction.
  5. 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, record keyword.
  • Kotlin data class val.
  • Python frozen=True dataclass; tuple, frozenset.
  • C# records, readonly struct, ImmutableList.
  • Rust default-immutable; mut is 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.

Further reading#

Curated, high-credibility sources for going deeper on this topic.