Symptom

Custom collections (ring buffer, skip list, B-tree, linked list, sparse array) expose their internal representation to consumers who want to walk them. Every traversal site duplicates the internal-structure knowledge; refactoring the collection's representation breaks every consumer in lockstep.

Goal

The collection exposes one iterator-shaped surface (next() / done); consumers use it without knowing the underlying representation. Replacing the representation (array-backed to linked-list-backed) is a one-file change inside the collection.

Before the pattern

class RingBuffer {
constructor(capacity) {
this.buffer = new Array(capacity);
this.head = 0;
this.tail = 0;
this.size = 0;
this.capacity = capacity;
}
push(item) { /* ... */ }
}
function dump(buf) {
let i = buf.head;
let count = 0;
while (count < buf.size) {
console.log(buf.buffer[i]);
i = (i + 1) % buf.capacity;
count++;
}
}
// Every consumer that wants to iterate must know head/tail/capacity
// and the modular arithmetic.

After the pattern

class RingBuffer {
constructor(capacity) {
this.buffer = new Array(capacity);
this.head = 0;
this.tail = 0;
this.size = 0;
this.capacity = capacity;
}
push(item) { /* ... */ }
[Symbol.iterator]() {
let i = this.head;
let count = 0;
const that = this;
return {
next() {
if (count >= that.size) return { done: true, value: undefined };
const value = that.buffer[i];
i = (i + 1) % that.capacity;
count++;
return { done: false, value };
},
};
}
}
for (const item of ringBuffer) {
console.log(item);
}
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 5. The book uses a List + ListIterator pair; this JavaScript adaptation uses the language's built-in Symbol.iterator protocol on a ring buffer because the encapsulation benefit (hiding modular arithmetic) reads more concretely than a generic List.
Pressure

Per-consumer traversal logic is Insider Trading on the collection's internals — Message Chains (`buf.buffer[buf.head]`) that walk through fields the collection should encapsulate. Changing head/tail semantics or storage layout forces editing every consumer.

Tradeoff

Iterators allocate per-traversal (an iterator object per for-of loop); in tight inner loops this matters. They also obscure performance characteristics — a consumer cannot tell from `for (const x of collection)` whether the iterator is O(1) per step or O(log n).

Relief

Consumers read as `for (const x of collection)`; the collection's representation changes without touching consumers; performance and correctness optimizations on the iteration live in one place; the language's iteration protocols (for-of, spread, destructuring) work out of the box.

Trap

Iterators that mutate the collection underneath (allow .push() during iteration, or invalidate on .delete()) produce inconsistent traversal behaviour that no test exercises completely. Either freeze the collection during iteration or document mutation semantics; ad-hoc behaviour is a footgun.