Overview
An immutable object cannot change after construction. To 'modify' it, you create a new object with the changed value. Immutable objects are simpler to reason about (no hidden changes), inherently thread-safe (no synchronization needed), safe to share and cache, and make great map keys.
In practice: final/readonly fields, no setters, defensive copies of mutable inputs, and 'with' methods returning new instances. Java records, Kotlin data classes, TypeScript readonly types, and libraries like Immer or persistent collections make immutability convenient. Mutable state still exists in systems, but it is concentrated in a few well-controlled places.
Once printed, a receipt never changes. If there is a correction, the store issues a new receipt. Anyone holding the original can trust it.
When to use it
- Value objects: Money, Date ranges, Coordinates.
- Data shared across threads.
- Configuration and keys in maps or caches.
- Event and message payloads.
Where it shows up in interviews
Recognize it when: shared data across threads.
- Design a thread-safe config service
- Design a concurrent cache
Recognize it when: money, time ranges, coordinates.
- Design a booking system with time slots
- Design Splitwise amounts
Where it is used in real software
LocalDate, Instant, and Duration are immutable; plusDays returns a new object.
Strings are immutable, which allows interning and safe sharing.
State updates create new objects so changes are detected by reference comparison.
Key terms
- Immutable object
- State cannot change after construction.
- Defensive copy
- Copy mutable inputs and outputs.
- Wither
- withX() returns a modified copy.
- Persistent data structure
- Shares structure between versions efficiently.
- Shallow immutability
- Fields cannot be reassigned, but referenced objects may still change.
How it works, step by step
- 1Make fields final/readonly
Assign once in the constructor.
- 2Remove setters
Provide withX() methods instead.
- 3Copy mutable inputs
List.copyOf, spread operators.
- 4Return unmodifiable views or copies
Never internal mutable collections.
- 5Make the class final
Prevent subclasses adding mutability.
Mutable vs immutable date range
A booking holds a DateRange that another piece of code 'adjusts'
| Scenario | Mutable DateRange | Immutable DateRange |
|---|---|---|
| Code extends range for a preview | Booking silently changes | New object; booking unchanged |
| Two threads read and write | Race conditions | No synchronization needed |
| Used as a map key | Key changes, lookup breaks | Hash stays stable |
NOWScenario: Code extends range for a preview | Mutable DateRange: Booking silently changes | Immutable DateRange: New object; booking unchanged
Immutability removes a whole class of aliasing and concurrency bugs.
Implementation
public record DateRange(LocalDate start, LocalDate end) { public DateRange { if (end.isBefore(start)) throw new IllegalArgumentException("end before start"); } public long nights() { return ChronoUnit.DAYS.between(start, end); } public boolean overlaps(DateRange o) { return start.isBefore(o.end) && o.start.isBefore(end); } public DateRange extendBy(int days) { return new DateRange(start, end.plusDays(days)); } // new object} public final class Cart { private final List<String> skus; public Cart(List<String> skus) { this.skus = List.copyOf(skus); } // defensive copy public List<String> skus() { return skus; } // already unmodifiable public Cart add(String sku) { var next = new ArrayList<>(skus); next.add(sku); return new Cart(next); }}Complexity and performance
Persistent structures reduce this.
Immutable data is thread-safe.
Trade-offs
Creating new objects on every change costs memory and GC for large, frequently updated structures.
Deep updates of nested immutable data are verbose without helpers (Immer, lenses).
Variants and related techniques
Mutable builder, immutable product.
CopyOnWriteArrayList for read-mostly concurrent data.
Common mistakes
- final fields referencing mutable lists.
Fix: Copy into unmodifiable collections.
- readonly in TypeScript assumed to be deep.
Fix: readonly is compile-time and shallow; use Readonly types recursively or freeze.
Interview questions
Why are immutable objects thread-safe?
Their state never changes after construction, so threads can share them without locks and never see partially updated state.
How do you make a Java class immutable?
Make it final, fields private final, no setters, validate in the constructor, defensively copy mutable inputs, and return unmodifiable or copied collections. Records cover most of this.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Make an Order immutable with withers | Easy | Copy on change. |
| Refactor a shared mutable config to immutable snapshots | Medium | Concurrency. |