Immutability
Never modify existing data — create a new value instead. Immutable data is safe to share, easy to reason about, and eliminates a whole class of mutation bugs.
★★★★★4/5Inside a codebase — classes, modules, files
How it works
An immutable value, once created, cannot change. Instead of mutating an object in place, you produce a new object with the desired changes. The original remains intact.
Immutability eliminates shared-state bugs. When multiple threads or components hold a reference to an object, any one of them mutating it creates race conditions, stale views, and hard-to-reproduce bugs. With immutable data, sharing is safe by definition — no one can corrupt the value you hold.
Immutability also makes change tracking trivial: if an object is different from a previous version, it's a different reference. React's performance optimisation (shallow equality checks), Redux's state diffing, and Git's object model all rely on this property.
Languages handle immutability differently: Rust makes ownership and immutability the default; Haskell enforces it everywhere; JavaScript/TypeScript require discipline (const, Object.freeze, Immer, readonly types). Go uses value semantics for structs — passing by value copies, so the original is safe.
Implementation
TypeScript · Go · Rust// ❌ Mutable — modifies the original object; sharing is unsafe
interface Cart { items: string[]; total: number; }
function addItem(cart: Cart, item: string, price: number): Cart {
cart.items.push(item); // ❌ mutates caller's cart
cart.total += price;
return cart;
}
// ✓ Immutable — always return a new value; original is untouched
interface ImmutableCart {
readonly items: readonly string[];
readonly total: number;
}
function addItem(cart: ImmutableCart, item: string, price: number): ImmutableCart {
return {
items: [...cart.items, item], // new array
total: cart.total + price, // new total
};
}
const cart1 = { items: ["book"], total: 12 };
const cart2 = addItem(cart1, "pen", 3);
console.log(cart1.items); // ["book"] — unchanged, safe to share
console.log(cart2.items); // ["book", "pen"]Why it matters
Shared mutable state is the root cause of the most painful bugs: race conditions, stale cache values, unexpected side effects. Immutability solves this by making sharing safe — a value that can't change can be referenced from anywhere without coordination.
✓ When to use
- →Domain model values that should not change (Money, DateRange, Address)
- →Data shared across threads, goroutines, or async tasks
- →Event sourcing and audit logs — events must never be modified
- →React state, Redux store — immutable updates enable efficient diffing
✗ When NOT to use
- →Hot performance-critical paths where copying is the bottleneck (low-level game loops, number-crunching)
- →Large data structures where copying is prohibitively expensive (use persistent data structures instead)
Trade-offs
Sharing is always safe — no coordination required
Creating new objects on every change has a GC and memory cost
Change is explicit — you know when and where new values are created
Updating deeply nested structures is verbose without helper libraries (Immer, Lens)
Enables cheap equality checks by reference
Requires discipline in languages that don't enforce it (JS, Go, Python)
In production
Reducers return new state — never mutate the previous state. Enables time-travel debugging and DevTools replay
Log records are immutable — once appended, never changed. Consumers replay from any offset safely
Every commit, tree, and blob is an immutable content-addressed object. History is append-only and tamper-evident
Industry adoption
Related principles
Pure Functions
A pure function always produces the same output for the same input and causes no side effects — making it predictable, testable, and safe to run anywhere.
Higher-Order Functions
Functions that accept other functions as arguments or return them — enabling composable, reusable abstractions without classes or inheritance.
Event Sourcing
LiveStore state as an immutable log of events rather than the current snapshot — rebuild state by replaying events.
Observer Pattern
Let a subject notify a dynamic list of dependents automatically when its state changes — without the subject knowing who is listening.