What is representation exposure, and how does returning an internal mutable collection or storing a caller-supplied object break a class's invariants?
answer
- Live list handed out = invariant dead
- Copy in, copy/view out
- Copy BEFORE validate (TOCTOU)
- Copies are shallow
- Immutable state needs no defense
basics
~20 sRepresentation exposure is handing out a reference to your internal mutable data. If a method returns the live list, or the constructor stores the caller's list directly, the caller can change that data afterwards without going through your validation, so your rules stop holding.
solid answer
~50 sAn object's invariants only hold if the object controls every path that mutates its state. Two leaks break that control. **Outbound**: a getter returns the live internal collection, array, date, or builder — the caller mutates it and bypasses every check. **Inbound**: the constructor or a setter stores a reference the caller still holds (aliasing), so the caller can mutate it later; identically, storing a mutable object then handing the same reference to two owners. The standard defenses: return an unmodifiable view or a defensive copy on the way out, and copy on the way in before validating (validate the copy, not the original — otherwise a concurrent caller can swap the contents between the check and the store, a time-of-check/time-of-use bug). Best of all, store immutable types: nothing to copy, nothing to leak. Costs are real — copies allocate, unmodifiable views fail at runtime rather than compile time — so scope the defense to types that actually own invariants or cross a trust boundary.
code
pseudocode · 13 linesclass Order {
private items // invariant: size <= 10, no duplicates
constructor(initial) {
this.items = copyOf(initial) // 1) copy first
validate(this.items) // 2) validate the copy, not the caller's
}
items() = unmodifiableView(this.items) // or copyOf(...) for a snapshot
add(item) { require(this.items.size < 10 && !this.items.has(item))
this.items.add(item) }
}go deeper
Say returning the internal list lets callers modify it directly, skipping your checks; fix by returning a copy or read-only view.
Cover both directions (getter leak and constructor aliasing), name defensive copying and unmodifiable views, and state the copy-then-validate order.
Add TOCTOU reasoning, shallow-vs-deep copy limits, the cost/benefit of copying on hot paths, and the alternative of not exposing the collection at all so the collection type also stays hidden.
Frame it as a boundary policy: which types are trusted, where copies are mandated, what the type system or language can enforce for free, and the security/concurrency consequences of shared mutable references crossing module lines.
## Terms - **Reference/aliasing**: two variables pointing at the same object. Mutating through one is visible through the other. - **Representation exposure** (also called *rep exposure*): a class lets a reference to its internal mutable state escape, or adopts a reference the outside world still holds. - **Defensive copy**: making your own copy of a mutable value at the boundary so no one else holds a reference to yours. - **Unmodifiable view**: a read-only wrapper around your collection — cheap, but it is a *view*: if you mutate the underlying collection, the holder sees the change, and mutation attempts through the view fail at runtime, not compile time. - **Immutable copy**: a snapshot that can never change, from either side. ## The two leaks **Outbound leak.** A class holds `items` and enforces "at most 10 items, no duplicates". `getItems()` returns the field. A caller writes `order.getItems().add(x)` eleven times. No exception, no validation, invariant dead. Worse, the corruption is discovered far from its cause — the stack trace points at the reader, not the mutator. **Inbound leak (aliasing capture).** `new Period(start, end)` stores the caller's mutable date objects. The caller keeps its reference and later calls `end.setTime(...)`, moving the period's end into the past. Same for `setItems(list)` storing the caller's list. The object was valid at construction and became invalid without any method being called on it. **TOCTOU variant.** If you validate the caller's argument and *then* copy it, a second thread (or a re-entrant callback, or a subclass overriding a method called during validation) can change the contents in between, so you validate one value and store another. The rule is: **copy first, validate the copy, store the copy.** Order matters. ## Defenses, in order of preference 1. **Use immutable types for state.** Immutable dates, value objects, persistent/immutable collections. Nothing can change, so nothing needs guarding, and thread safety comes free. 2. **Don't expose the collection at all.** Replace `getItems()` with `itemCount()`, `contains(x)`, `forEachItem(fn)`, or return a stream/iterator that cannot mutate. This also keeps you free to change the collection type later. 3. **Return an unmodifiable view** when callers really need to iterate. Cheap and allocation-free, but note the view still reflects later internal changes (which can surprise a caller who assumed a snapshot) and rejects writes only at runtime. 4. **Return a copy** when the caller needs a stable snapshot. Costs an allocation proportional to size. 5. **Copy on input** for any mutable argument you retain, then validate the copy. Note that copies are usually **shallow**: copying a list of mutable objects protects the list structure but not the elements. If the elements are mutable and the invariant depends on them, you need immutable elements or a deep copy. ## When not to bother - Types with no invariants at all (an internal DTO passed between two functions in the same module). - Hot paths where the copy dominates cost and the code on both sides is trusted and reviewed — but document the sharing explicitly rather than leaving it implicit. - Languages/ecosystems where the type system does the work: value semantics with copy-on-write, ownership/borrowing rules, or read-only reference types make the leak unrepresentable rather than merely discouraged. ## Why it matters beyond correctness - **Security**: an attacker-controlled object retained by a security-sensitive class (a permission set, a signed payload, a file path) is a classic check-then-modify exploit — the value passes validation and is mutated before use. - **Concurrency**: shared mutable state that escaped is the source of most data races; hiding the representation is often the cheapest thread-safety strategy. - **Evolvability**: returning your actual collection type also publishes that type. Callers start depending on its exact behavior, and swapping it becomes a breaking change even though it was 'internal'.
- When is an unmodifiable view a worse choice than a copy?When the caller needs a stable snapshot — the view keeps reflecting your later mutations, which can surprise code that iterates it across time or hands it to another thread. Use a copy (or an immutable collection) whenever the caller's expectation is 'the contents as of now'.
- Why must the defensive copy be made before validation, not after?Because between your check and your store, another thread or a re-entrant call can change the caller's object — a time-of-check/time-of-use race. Copying first means you validate exactly the bytes you will retain.
- Does making a class immutable eliminate representation exposure entirely?Only if the state it holds is itself immutable all the way down. An immutable wrapper around a mutable array or list still leaks if that reference escapes or was captured from a caller.
Giving out your internal list is like giving a visitor the master key so they can 'look at' a room. Whatever the rules on the wall say, they can now rearrange the furniture whenever they like, and you will find out only when something breaks.