Programming Techniques

Immutability

Treating data as values that never change in place, only replaced with new versions.

Overview

Immutability means data, once created, cannot be changed in place. Instead of modifying existing structures, new versions are produced. This eliminates an entire class of bugs caused by shared mutable state, makes data flow traceable, and enables concurrency without locking.

Origin

Immutable data is fundamental to functional programming, tracing back to LISP. It gained mainstream traction through React's state model (2013), Redux, and languages like Clojure and Rust which enforce immutability by default.

Examples

Immutable update patterns in JavaScript

// Mutable, modifies the original
function addItem(cart, item) {
  cart.items.push(item)  // mutates cart
  return cart
}

// Immutable, produces a new object
function addItem(cart, item) {
  return {
    ...cart,
    items: [...cart.items, item],
    updatedAt: new Date().toISOString(),
  }
}

// Nested immutable update (deep)
function updateOrderStatus(state, orderId, status) {
  return {
    ...state,
    orders: state.orders.map(order =>
      order.id === orderId
        ? { ...order, status }
        : order
    )
  }
}

Freeze and structural sharing

// Object.freeze enforces immutability at runtime (shallow)
const config = Object.freeze({
  db: Object.freeze({ host: 'localhost', port: 5432 }),
  cache: Object.freeze({ ttl: 300 }),
})

config.db.host = 'production'  // silently fails (throws in strict mode)

// Immer library, write mutations, get immutable output
import { produce } from 'immer'

const nextState = produce(currentState, draft => {
  draft.orders[2].status = 'shipped'  // safe, draft is a proxy
})
// currentState is unchanged; nextState is a new object

Use Cases

  • 01Redux and other flux-pattern state managers that rely on reference equality for change detection
  • 02Concurrency: immutable data can be shared across threads/workers without locking
  • 03Undo/redo: storing previous versions is trivial when nothing is mutated in place
  • 04Caching: immutable keys guarantee cache entries never go stale due to mutation
  • 05Debugging: state at any point in time can be logged and replayed

When Not to Use

  • //Performance-critical paths where copying large structures on every update is too expensive
  • //Very large mutable buffers (image processing, audio) where in-place mutation is essential
  • //Simple scripts where the overhead of immutable patterns adds complexity without benefit

Technical Notes

  • Structural sharing (used by Immutable.js and Clojure persistent data structures) means new versions share unchanged subtrees, keeping memory cost low
  • JavaScript Object.freeze is shallow, nested objects remain mutable unless also frozen
  • Rust enforces immutability by default: variables are immutable unless declared with mut
  • React's reconciler uses reference equality to detect changes. Mutating state in place breaks this because the reference does not change even though the data did