Immutability — once data is created, it does not change — eliminates a category of bugs that mutable data permits. Race conditions on shared state, "spooky action at a distance" where one piece of code modifies data another is reading, defensive copying needed everywhere. Immutability eliminates all of it at the cost of some allocation overhead and some indirection.
For most data in most modern systems, immutable is the right default. This page is about the language-specific patterns for working with immutable data, the cases where mutable design is genuinely better, and the practical implementation patterns.
Mutable data has three problems:
Immutable data eliminates all three. The "cost" — having to create a new structure when something changes — sounds expensive but is usually invisible in modern systems with good GC and structural sharing.
// Records are immutable by default
record Order(String id, BigDecimal amount, OrderStatus status) {}
// Updating: produce a new record
Order updated = new Order(original.id(), original.amount(), OrderStatus.SHIPPED);
Plus immutable collections: List.of(...), Map.of(...), the immutable variants in Collections.unmodifiableXxx.
For larger structures, libraries like Vavr or Eclipse Collections provide proper functional collections.
# NamedTuple, frozen dataclass, or immutable Pydantic models
from dataclasses import dataclass
@dataclass(frozen=True)
class Order:
id: str
amount: Decimal
status: OrderStatus
Tuples and frozensets are built-in immutable collections; lists and dicts are mutable but can be defensively copied.
// readonly properties
type Order = {
readonly id: string;
readonly amount: number;
readonly status: OrderStatus;
};
// Updates with spread
const updated: Order = { ...original, status: 'SHIPPED' };
Library support: Immer, Immutable.js, fp-ts.
Immutable by default — let x = 5 is immutable; let mut x = 5 is mutable. The compiler enforces. The whole system is designed around immutability + ownership.
val (immutable) vs. var (mutable). data class for immutable records. Standard collections have separate mutable/immutable variants (List vs. MutableList).
Naively, "produce a new copy on each change" sounds expensive. In practice, immutable collections use structural sharing — the new structure shares most of its data with the old, only the modified portion is new.
A 1000-element immutable list updated at index 500 doesn't copy 1000 elements; it shares both halves and references the new element. The cost is logarithmic in the structure size, not linear.
This is what makes immutable collections practical for production systems.
Order, Customer, Address — these have identity and lifetime, but their attributes do not change in place. They get replaced with new versions.
Configuration data is set at startup and read throughout. Mutating it during runtime is almost always a bug.
A cache that returns a mutable object risks the caller modifying the cached value. Returning immutable values eliminates the risk.
Anything shared between threads. Immutable + safe publication is dramatically simpler than locks.
A function that takes a list and "modifies" it has implicit contracts ("can I keep using the list afterward?"). A function that takes an immutable list cannot modify it; the caller's assumptions are protected.
Inner loops, large data manipulation. The allocation overhead matters; explicit mutation is faster.
Building a large structure incrementally, where the intermediate states are not exposed externally. A StringBuilder (mutable) is correct; a string-concatenation chain (immutable) is N²-time.
Within a function, a counter or accumulator can be mutable without affecting anyone else.
Some libraries are built around mutation (StringBuilder, ByteBuffer). Wrap them at the boundary with immutable interfaces if needed; do not fight them internally.
For records or value objects with many fields, a builder lets you construct incrementally without making the result mutable.
Order order = Order.builder()
.id("abc")
.amount(new BigDecimal("99.99"))
.status(OrderStatus.PENDING)
.build();
Producing modified copies:
Order shippedOrder = order.withStatus(OrderStatus.SHIPPED);
Each withX returns a new instance with one field changed.
When interacting with mutable APIs, copy on the way in and the way out:
public ImmutableData(MutableSource source) {
this.data = List.copyOf(source.data); // copy on the way in
}
public List<X> getData() {
return List.copyOf(this.data); // copy on the way out
}
This is the price of working with mutable code; minimize the surface area.
Libraries that provide proper functional collections with structural sharing. Vavr, Eclipse Collections (immutable), Immutable.js, Clojure-style structures.
For systems doing significant immutable manipulation, these are dramatically faster than copy-on-every-change.
getList() method that returns a mutable list defeats the purpose.StringBuilder-style operations are fine to mutate.For an existing codebase moving toward more immutability:
The change does not need to be all-at-once. Each immutable boundary added pays off independently.