Immutability vs Mutability
Whether your data structures should be unchangeable once created or freely modified in place. A core design decision that shapes concurrency safety, debuggability, and performance across every language and codebase you touch.
The short answer
Immutability over Mutability for most cases. Immutability eliminates the entire class of bugs caused by shared state changing under your feet — aliasing, race conditions, and "who mutated this?".
- Pick Immutability if write concurrent code, value debuggability, or want defensive copies to disappear. Default to immutable; reach for mutation only when a profiler tells you to
- Pick Mutability if in a measured hot path — tight numeric loops, large buffers, game state at 60fps — where allocation churn actually shows up in a flame graph
- Also consider: This isn't all-or-nothing. The pros use immutable interfaces with mutable internals (builders, transients, copy-on-write). Pick immutability at the boundary, allow mutation locally where it's contained and proven necessary.
— Nice Pick, opinionated tool recommendations
The case for Immutability
An immutable value is a fact: once created, it never lies to you. No method call can change it behind your back, so you can pass it across threads, cache it, use it as a map key, and reason about it without tracing every reference that touches it. This kills the nastiest bugs in software — aliasing surprises, data races, and the 2am 'but I never changed that' hunt. It makes equality meaningful, undo/redo trivial, and time-travel debugging possible. React, Redux, Clojure, Rust's borrow checker, and functional cores in otherwise-imperative codebases all lean on it for a reason. The objection is always performance: copying instead of mutating allocates. But persistent data structures with structural sharing make 'copies' O(log n), not O(n), and modern allocators are fast. You pay a little to delete an entire category of failure. That trade is almost always correct.
The case for Mutability
Mutation is how hardware actually works. Memory is a giant mutable array, and pretending otherwise has a cost: every 'change' to an immutable structure allocates, and allocation plus garbage collection is not free. In tight loops — image processing, physics simulation, parsing megabytes, accumulating into a buffer — in-place mutation is the difference between 16ms and 160ms per frame. A mutable StringBuilder beats immutable string concatenation by orders of magnitude. Mutability also maps cleanly onto inherently stateful domains: a game entity, a connection pool, a counter. Forcing immutability there produces awkward, ceremony-heavy code that rebuilds the world to flip one flag. The honest argument isn't that mutation is safe — it isn't. It's that within a single owner, a contained scope, or a profiled hot path, mutation is simpler and dramatically faster, and dogma shouldn't override the flame graph.
Where Mutability bites you
Shared mutable state is the original sin of concurrent programming. The bugs it produces are the worst kind: nondeterministic, unreproducible, and invisible in code review. Two threads touch one object, you forget a lock, and you ship a heisenbug that surfaces once a week in production and never in your tests. Even single-threaded, mutation leaks: you hand a list to a function, it mutates it, and now your caller's data is wrong three stack frames away. So everyone starts defensive-copying 'just in case' — and congratulations, you're now copying more than immutability ever would, with none of the guarantees. Mutable objects also can't be safely cached or used as keys, because their hash changes underneath the hash map. The cost of mutation isn't the mutation; it's the eternal vigilance it demands from every human who ever reads the code after you.
The verdict
Default to immutability and make mutation the exception you justify, not the rule you assume. Most code is glue, not hot loop — and in glue, the value of 'this cannot change under me' dwarfs the cost of an extra allocation nobody will ever measure. Make your data immutable at every boundary: function arguments, return values, anything shared across threads or modules. Then, where a profiler proves a hot path matters, drop to local, contained mutation — a builder, a transient, a mutable buffer that never escapes its scope. That's exactly what Clojure, Rust, and React's best practitioners do. The wrong move is reaching for mutation everywhere because it 'feels faster,' then drowning in defensive copies and concurrency bugs. Immutability is the safe default that scales with your team's size and your codebase's age. Mutability is a sharp tool — earn it with a measurement, don't grab it out of habit.
Quick Comparison
| Factor | Immutability | Mutability |
|---|---|---|
| Concurrency safety | Free — immutable values are inherently thread-safe, no locks needed | Dangerous — shared mutable state causes races, requires locking discipline |
| Raw performance in hot paths | Allocates on every change; mitigated by structural sharing but not free | In-place updates, zero allocation, wins tight loops and large buffers |
| Debuggability | State can't change behind your back; trivial to reason about and time-travel | 'Who mutated this?' archaeology; bugs surface far from their cause |
| Fit for stateful domains | Awkward for counters, game entities, pools — rebuilds to flip a flag | Maps naturally onto inherently stateful, single-owner objects |
| Cost over a codebase's lifetime | Safe default that scales with team size and code age | Demands eternal vigilance and defensive copying from every reader |
The Verdict
Use Immutability if: You write concurrent code, value debuggability, or want defensive copies to disappear. Default to immutable; reach for mutation only when a profiler tells you to.
Use Mutability if: You're in a measured hot path — tight numeric loops, large buffers, game state at 60fps — where allocation churn actually shows up in a flame graph.
Consider: This isn't all-or-nothing. The pros use immutable interfaces with mutable internals (builders, transients, copy-on-write). Pick immutability at the boundary, allow mutation locally where it's contained and proven necessary.
Immutability vs Mutability: FAQ
Is Immutability or Mutability better?
Immutability is the Nice Pick. Immutability eliminates the entire class of bugs caused by shared state changing under your feet — aliasing, race conditions, and "who mutated this?" archaeology. The performance cost is real but usually marginal and increasingly erased by structural sharing and persistent data structures. Mutability wins narrow, hot loops; immutability wins the other 95% of your code where correctness and readability matter more than shaving allocations.
When should you use Immutability?
You write concurrent code, value debuggability, or want defensive copies to disappear. Default to immutable; reach for mutation only when a profiler tells you to.
When should you use Mutability?
You're in a measured hot path — tight numeric loops, large buffers, game state at 60fps — where allocation churn actually shows up in a flame graph.
What's the main difference between Immutability and Mutability?
Whether your data structures should be unchangeable once created or freely modified in place. A core design decision that shapes concurrency safety, debuggability, and performance across every language and codebase you touch.
How do Immutability and Mutability compare on concurrency safety?
Immutability: Free — immutable values are inherently thread-safe, no locks needed. Mutability: Dangerous — shared mutable state causes races, requires locking discipline. Immutability wins here.
Are there alternatives to consider beyond Immutability and Mutability?
This isn't all-or-nothing. The pros use immutable interfaces with mutable internals (builders, transients, copy-on-write). Pick immutability at the boundary, allow mutation locally where it's contained and proven necessary.
Immutability eliminates the entire class of bugs caused by shared state changing under your feet — aliasing, race conditions, and "who mutated this?" archaeology. The performance cost is real but usually marginal and increasingly erased by structural sharing and persistent data structures. Mutability wins narrow, hot loops; immutability wins the other 95% of your code where correctness and readability matter more than shaving allocations.
Related Comparisons
Disagree? nice@nicepick.dev