Immutability

On this page 7

Immutability is the property of a value or data structure that cannot be modified after it is created. In functional programming, any operation that appears to change data instead returns a new value, leaving the original intact, which makes programs easier to reason about, test and run concurrently.

Immutability is the foundation on which pure functions, safe closures and referential transparency rest. Languages such as Haskell, Clojure, Elm and Erlang make it the default, while multi-paradigm languages such as JavaScript, Java, Kotlin, Scala, C# and Rust offer immutable variables, types and collections as deliberate choices.

How immutability works in functional programming

In a mutable model, a variable names a memory location whose contents can change. In an immutable model, a name is bound to a value, and that value stays the same for its entire lifetime. Updating a record means constructing a new record with the changed field and keeping or discarding the old one.

Copying an entire structure for every change would be expensive, so functional runtimes use structural sharing. A new version reuses every unchanged part of the old version and allocates only the path that changed. Persistent data structures built on hash array mapped tries or balanced trees make an update to a large map cost about O(log n), with a branching factor of 32 keeping the tree only a few levels deep.

The following table compares the main ways immutability is implemented and what each costs.

StrategyHow an update worksTypical cost
Full copyDuplicate the whole structureO(n) time and memory per update
Structural sharingCopy only the changed pathAbout O(log n) per update
Copy-on-writeShare until someone writes, then copyCheap reads, costly first write
Transients or buildersMutate privately, then freezeNear-mutable speed for batch updates
Implementation strategies for immutable data

Levels of immutability

Immutability is not a single switch. A constant reference is not the same as an immutable value, and confusing the two is a frequent source of bugs. The table below separates the levels that languages offer.

LevelGuaranteeExample mechanisms
Immutable bindingThe name cannot be reassignedconst in JavaScript, final in Java, val in Kotlin
Shallow immutabilityTop-level fields cannot changeObject.freeze, read-only properties
Deep immutabilityNo reachable part can changeHaskell values, Clojure collections
Read-only viewThis reference cannot modify the dataReadonlyArray in TypeScript, unmodifiable wrappers
Ownership-based controlMutation allowed only through a unique referenceRust borrow checker, mut keyword
Levels of immutability and what they guarantee

A JavaScript const array can still be pushed to, because only the binding is fixed. A Java final field holding a List can still have elements added. Only deep immutability gives the full guarantees that functional programming relies on, and Rust reaches a similar safety level through controlled mutation rather than by forbidding it.

Why immutability matters

The main benefit of immutability is that values can be shared freely. Any function, closure or thread that receives an immutable value can rely on it never changing behind its back, so defensive copies and locks become unnecessary.

  • Referential transparency: an expression can be replaced by its value, which enables equational reasoning and safe refactoring.
  • Thread safety: immutable data needs no synchronization, which removes whole classes of race conditions.
  • Cheap change detection: comparing references instead of contents tells a UI framework whether state changed.
  • History and undo: keeping old versions is free with structural sharing, enabling undo stacks and time-travel debugging.
  • Safe caching and memoization: results keyed on immutable inputs never become stale.

These advantages connect immutability to its neighboring Terms. A pure function can only be pure if its inputs cannot be mutated by someone else, and a closure that captures immutable values behaves predictably no matter when it runs.

Several concepts overlap with immutability but answer different questions. The table clarifies how they relate.

ConceptQuestion it answersRelationship to immutability
Pure functionDoes the function avoid side effects?Requires immutable inputs to be reliable
Persistent data structureAre old versions still available?Efficient implementation of immutability
Value objectIs equality defined by content?Usually immutable by design
ConstantIs a name fixed at compile time?Narrower, covers bindings only
EncapsulationWho may change the state?Restricts mutation instead of removing it
Immutability and neighboring concepts

In practice, functional programs are rarely free of mutation entirely. The common pattern is a functional core with an imperative shell: business logic works on immutable values, while a thin outer layer performs input, output and database writes.

Costs, trade-offs and common mistakes

Immutability is not free. Each update allocates new objects, which increases garbage collection work, and persistent structures have worse constant factors and cache locality than plain arrays. In tight numerical loops, mutable arrays can be several times faster.

ProblemCauseRemedy
Unexpected mutationOnly the binding was made constantUse deeply immutable types or freeze recursively
Slow bulk updatesMany small immutable updates in a loopUse transients, builders or a single bulk operation
Memory growthOld versions kept alive by referencesRelease history that is no longer needed
Verbose update codeNested records copied by handUse lenses, copy methods or update helpers
Common problems and how to address them

The pragmatic rule is to make immutability the default and introduce mutation locally, behind a clear boundary, only where profiling shows it is needed.

A plain-language summary

Immutability means that once a piece of information is written down, nobody erases or overwrites it. If something needs to change, a new version is written instead, and the old one stays exactly as it was.

It works like a signed contract. To change the terms, the parties sign an amendment rather than scribbling over the original. Everyone holding a copy can trust that it will not change under them, which prevents a great deal of confusion when many people, or many parts of a program, use the same information.

An example from practice

A team builds a web application whose interface state, including the user profile, a list of 2,000 products and the shopping basket, lives in a single immutable state tree. When a user adds an item, a reducer function returns a new state object that reuses the unchanged product list and replaces only the basket.

Because unchanged parts keep the same reference, the rendering library compares references and skips re-rendering the product list entirely. The team also gains an undo button and time-travel debugging, since every previous state still exists. A bug where two components had silently shared and modified the same basket array disappears completely, because no component can modify the array any longer.