Immutabilitymedium3-5 years

A reviewer approves `record Cart(String id, List<Item> items) {}` as immutable because every field is final. Months later, `cart.items().add(newItem)` works — no exception, and the cart really does have one more item. What actually happened, and what's the minimum fix inside the record's constructor?

final on Cart's items field only stops that field from ever being reassigned to point at a different List — it says nothing about what happens to the List object it already points at. cart.items() is the record's auto-generated accessor, and by default it just hands back that exact same List reference; if the caller who built the Cart still holds their own reference to it (or the accessor is called and the result stored), calling .add() on that reference mutates the one and only List object both the caller and the record are pointing at — the record's own fields haven't changed at all, items still points at the exact same object, that object's contents have. A record gives you shallow immutability — the fields themselves can't be reassigned — for free; deep immutability, where nothing reachable from the record can change, needs the constructor to actually copy any mutable component into something that can't be mutated, most simply with List.copyOf(items).

The lesson behind it →