Design patternBehavioral Patterns
Iterator
Walk the elements of a collection one at a time through a standard interface, without exposing how the collection stores or fetches them.
- Complexity
- Medium
- Seen in the wild
- High
- Also known as
- Cursor
Intent
Give a collection a separate object that knows how to step through its elements and remembers where it is. Callers ask for the next element until there are none left, and never see whether the data lives in an array, a tree, a file, or a paginated remote API.
Forces
- Simple loops for callers vs complex storage behind them: a caller wants
for each invoice, not cursor tokens and page sizes. - Lazy access vs loading everything: fetching ten thousand records to find the first overdue one wastes time and memory.
- Several traversals at once vs position stored in the collection: if the collection holds the cursor, two loops over it corrupt each other.
- Hiding the representation vs letting callers stop early, filter, or map: the iteration contract must stay small and still compose.
Therefore
Move the traversal state (current position, buffer, cursor) out of the collection into an iterator object that implements the language's standard iteration protocol.
The collection exposes a factory method that returns a fresh iterator, so each loop gets its own position. Callers use the language's native loop (for...of, for ... in, range, enhanced for) and stop whenever they want. In most modern languages you should write the iterator as a generator function (function*, yield, iter.Seq). Write an explicit iterator class only when you need extra methods, explicit resource cleanup, or a language without generators.
Structure
Diagram source (Mermaid)
classDiagram
class IterableCollection {
<<interface>>
+iterator() Iterator
}
class Iterator {
<<interface>>
+next() Invoice
+hasNext() bool
}
class Invoices {
-InvoiceApi api
+iterator() Iterator
}
class InvoiceIterator {
-Invoice[] buffer
-String cursor
+next() Invoice
+hasNext() bool
}
class InvoiceApi {
+fetchPage(cursor) Page
}
IterableCollection <|.. Invoices
Iterator <|.. InvoiceIterator
Invoices ..> InvoiceIterator : creates
InvoiceIterator --> InvoiceApi
Invoices --> InvoiceApiParticipants
- Iterator (
Iterator): the language's protocol for "give me the next element, or tell me you're done". Use the built-in one instead of defining your own. - Concrete iterator (
InvoiceIterator): holds the traversal state and fetches more data when its buffer runs dry. - Iterable collection (
IterableCollection): the protocol that says "I can produce an iterator". - Concrete collection (
Invoices): returns a new iterator on each call, so traversals stay independent. - Client: loops with the language's native syntax and can stop at any element.
Use it when
- The data comes from pages, cursors, streams, or files, and callers should see one flat sequence.
- The collection has a non-trivial structure (tree, graph, sparse matrix, ring buffer) and callers should not depend on it.
- You need several traversal orders over the same data, such as depth-first and breadth-first, newest-first and oldest-first.
- Callers often stop early, and producing all elements up front is expensive.
- Two pieces of code must traverse the same collection at the same time without interfering.
Do not use it when
- The data is already an array, list or map in memory. Return it, or return a read-only view, and let callers use the native loop.
- The traversal is a linear walk where the only state is a cursor. Write a generator function (
function*in TypeScript,yieldin Python,iter.Seqin Go 1.23+, aStreamorSpliteratorin Java); every modern language ships them, and they give the same laziness with a third of the code. A hand-written iterator class earns its place when iteration state outgrows a cursor - bookmarks, multiple simultaneous positions, or state callers must inspect and close. - Callers always need every element and the set is small. A function that returns a list is simpler to test and debug.
- You are inventing a custom
hasNext/nextinterface. Implement the language's protocol (Symbol.iterator,__iter__,java.util.Iterator) so the result works with every built-in loop and library. - The traversal needs to run an operation that differs per element type. That is Visitor, possibly fed by an iterator.
Application grill
Inspect first: the collections callers reach into directly (index access, .data, pagination params leaking into business code); the language's built-in iteration (generators, iterables, channels); how large result sets flow from the database today.
Missing pieces: the language's built-ins are the pattern; nothing to install. For database results, keyset pagination or a driver cursor, never OFFSET on large tables.
Ask before you build:
- Scale: what result sizes must stream instead of materialize (a memory bound per request)?
- Performance: does the walk hit the network per element or per page, and what page size fits?
- Reliability: what does the iteration do when the source changes mid-walk (an invalidated cursor)?
- Budget: unifying N ad-hoc loops into one iterator - who migrates the callers?
- Implementation: is the iterator lazy (pull) or buffered, and do errors surface at first pull or at construction?
Implementation steps
- Write down what the caller loops over today, and where the paging, indexing or recursion leaks into the calling code.
- Choose the language's standard iteration protocol. Prefer a generator; fall back to an iterator class if you need state you can inspect or close.
- Put all traversal state (index, cursor token, buffer, stack) in the iterator, not in the collection.
- Make the collection return a fresh iterator from its factory method on each call.
- Fetch lazily: load the next page or node only when the buffer is empty.
- Replace the caller's manual paging loop with the native loop, and add a test that stops early and checks how much data was fetched.
Example
A billing service exports invoices from a provider API that returns three invoices per page with a cursor to the next page. Callers loop over invoices without seeing pages, and stopping at the first overdue invoice fetches only the pages it needs.
interface Invoice { id: string; amountCents: number; overdue: boolean }
interface Page { items: Invoice[]; next?: string }
class InvoiceApi {
calls = 0;
constructor(private readonly all: Invoice[], private readonly pageSize: number) {}
fetchPage(cursor: string): Page {
this.calls++;
const start = Number(cursor);
const end = start + this.pageSize;
return { items: this.all.slice(start, end), next: end < this.all.length ? String(end) : undefined };
}
}
class InvoiceIterator implements Iterator<Invoice> {
private buffer: Invoice[] = [];
private cursor: string | undefined = "0"; // undefined means no more pages
constructor(private readonly api: InvoiceApi) {}
next(): IteratorResult<Invoice> {
while (this.buffer.length === 0 && this.cursor !== undefined) {
const page = this.api.fetchPage(this.cursor);
this.buffer = page.items;
this.cursor = page.next;
}
const item = this.buffer.shift();
return item ? { done: false, value: item } : { done: true, value: undefined };
}
}
class Invoices implements Iterable<Invoice> {
constructor(private readonly api: InvoiceApi) {}
[Symbol.iterator](): Iterator<Invoice> {
return new InvoiceIterator(this.api); // fresh state per loop
}
}
// The same traversal as a generator. Prefer this form in real TypeScript code.
function* invoices(api: InvoiceApi): Generator<Invoice> {
for (let cursor: string | undefined = "0"; cursor !== undefined; ) {
const page = api.fetchPage(cursor);
yield* page.items;
cursor = page.next;
}
}
const data = Array.from({ length: 7 }, (_, i) => ({
id: `INV-${i + 1}`,
amountCents: (i + 1) * 1000,
overdue: i === 4,
}));
const api = new InvoiceApi(data, 3);
for (const inv of new Invoices(api)) {
if (inv.overdue) {
console.log("first overdue:", inv.id); // INV-5
break;
}
}
console.log("pages fetched:", api.calls); // 2
let total = 0;
for (const inv of invoices(api)) total += inv.amountCents;
console.log(total, "pages fetched:", api.calls); // 28000 pages fetched: 5Trade-offs
Gains
- Callers use one native loop, whatever the storage or transport.
- Lazy fetching: early exits load only what was read.
- Each iterator has its own position, so traversals don't interfere.
- New traversal orders are new iterators; the collection does not change.
Costs
- A hand-written iterator class is more code than a generator and easier to get wrong (idempotent
hasNext, end-of-data handling). - Lazy iteration hides I/O inside a loop. Errors and latency show up far from the call that started them.
- Iterating while the underlying collection changes is undefined unless you design for it.
- For small in-memory collections, the indirection buys nothing.
Relations
- Composite trees are a common reason to write an iterator: depth-first or breadth-first over nested nodes.
- Visitor runs a type-specific operation on each element. An iterator supplies the elements.
- Factory Method is how the collection's
iterator()returns the right concrete iterator. - Memento can capture an iterator's position so a long export can resume after a crash.
Agent checklist
- You checked whether a generator or the language's built-in iteration covers the need before writing an iterator class.
- The iterator implements the language's standard protocol, not a custom
hasNext/nextinterface. - Traversal state lives in the iterator, and each call to the factory returns a fresh one.
- Data is fetched lazily, and a test stops early and asserts how many pages or nodes were loaded.
- End-of-data, empty collections and fetch errors are handled and tested.
See also
- CompositeTreat single objects and nested groups of them through one interface, so code can walk a tree of any depth without checking node types.
- VisitorAdd new operations over a fixed set of node types without editing those types, by routing each node to a type-specific method on a visitor object.
- Factory MethodLet a subclass or a supplied function decide which concrete object to create, so shared workflow code depends only on an interface.
- MementoCapture an object's internal state in an opaque snapshot so it can be restored later without exposing that state to the code holding the snapshot.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.