Data structures whose fields are reassigned across the codebase, with no clear owner of the mutation. Every reader of the data carries a different mental model of when it's valid; the side effect surface expands with every writer, and code that looked correct in isolation breaks when a different writer runs first or runs concurrently.
Mutation happens in one place behind a named function (or returns a new value), so the moment of change is clear. The encapsulating function owns the invariant; downstream readers compose against the guarantee rather than against the raw mutable shape, and concurrent reasoning becomes tractable because the mutation surface is one named call.
Smellier version
const order = { total: 100 };applyDiscount(order); // mutates totaladdTax(order); // mutates total
Fresher version
const order = { total: 100 };const final = addTax(applyDiscount(order));
Reasoning about state requires tracing every writer; cognitive load and the team's search cost both scale with writer count, and concurrent code becomes a hazard area. Reviewers pass changes that work for the call sequence in front of them; production runs a different sequence and produces behavior the reviewer never considered. The team's debugging cost compounds across the writer set.
Encapsulating mutation can force the team to learn a less ergonomic API for what was previously a direct write; immutability has migration cost in performance-sensitive code. The team trades maintenance cost for runtime overhead; in hot paths the trade may invert, and the team must choose between encapsulation discipline and per-operation performance.
Bugs stop being 'who set this field?' and become local to the encapsulating method. The team's verification cost drops because the audit surface contracts to one call instead of N writers, and downstream code composes against the encapsulating contract without re-checking the underlying mutation. Concurrent reasoning becomes possible because the moment of change is named.
Wrapping mutable fields with passthrough setters that change nothing — the leaky abstraction hides the same mutation everywhere behind syntactic sugar. Reviewers grant the safety presumption; runtime shows the underlying mutation still leaks through every direct field access that bypassed the setter, and the codebase carries an extra ceremony layer without the protection it advertised.