<arch.design/>
Principles/Immutability
{ }CodeArchitectureintermediate1958functionalvalue-objectsthread-safetycopy-on-write

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/5
{ }
Operates at: Code level

Inside 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

Redux

Reducers return new state — never mutate the previous state. Enables time-travel debugging and DevTools replay

Kafka

Log records are immutable — once appended, never changed. Consumers replay from any offset safely

Git

Every commit, tree, and blob is an immutable content-addressed object. History is append-only and tamper-evident

Industry adoption

4/5Widely adopted — mainstream at medium-to-large engineering orgs.

Related principles