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.
| Strategy | How an update works | Typical cost |
|---|---|---|
| Full copy | Duplicate the whole structure | O(n) time and memory per update |
| Structural sharing | Copy only the changed path | About O(log n) per update |
| Copy-on-write | Share until someone writes, then copy | Cheap reads, costly first write |
| Transients or builders | Mutate privately, then freeze | Near-mutable speed for batch updates |
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.
| Level | Guarantee | Example mechanisms |
|---|---|---|
| Immutable binding | The name cannot be reassigned | const in JavaScript, final in Java, val in Kotlin |
| Shallow immutability | Top-level fields cannot change | Object.freeze, read-only properties |
| Deep immutability | No reachable part can change | Haskell values, Clojure collections |
| Read-only view | This reference cannot modify the data | ReadonlyArray in TypeScript, unmodifiable wrappers |
| Ownership-based control | Mutation allowed only through a unique reference | Rust borrow checker, mut keyword |
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.
Immutability compared with related ideas
Several concepts overlap with immutability but answer different questions. The table clarifies how they relate.
| Concept | Question it answers | Relationship to immutability |
|---|---|---|
| Pure function | Does the function avoid side effects? | Requires immutable inputs to be reliable |
| Persistent data structure | Are old versions still available? | Efficient implementation of immutability |
| Value object | Is equality defined by content? | Usually immutable by design |
| Constant | Is a name fixed at compile time? | Narrower, covers bindings only |
| Encapsulation | Who may change the state? | Restricts mutation instead of removing it |
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.
| Problem | Cause | Remedy |
|---|---|---|
| Unexpected mutation | Only the binding was made constant | Use deeply immutable types or freeze recursively |
| Slow bulk updates | Many small immutable updates in a loop | Use transients, builders or a single bulk operation |
| Memory growth | Old versions kept alive by references | Release history that is no longer needed |
| Verbose update code | Nested records copied by hand | Use lenses, copy methods or update helpers |
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.