Iterator Pattern
Concept
Iterator separates the traversal of a collection from the collection's internal representation. Instead of exposing an array index, a linked-list node pointer, or a tree's internal structure to calling code, the collection hands out an iterator object that knows how to visit its elements one at a time via a small, uniform interface — typically hasNext()/next(), or a language's native iteration protocol. The caller writes one traversal loop shape regardless of whether the underlying structure is a flat array, a balanced tree walked in-order, a linked list, or a paginated remote API — the iterator absorbs that difference.
This is one of the few GoF patterns that essentially every modern language has folded directly into its syntax rather than leaving it as a library-level convention. Python's __iter__/__next__ protocol and generator functions, JavaScript/TypeScript's Symbol.iterator and for...of, Java's Iterable/Iterator interfaces backing its enhanced for loop, Go's range over slices/maps/channels (and iter.Seq since Go 1.23) — all of these are the Iterator pattern, just promoted from "a pattern you implement by hand" to "a protocol the language and its for loop understand natively." Writing a hand-rolled iterator class today, in a language with built-in iteration support, is usually a sign the wrong abstraction level was chosen rather than a legitimate application of the pattern.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
Language-native iterator protocol (generators, __iter__, Symbol.iterator, range) |
Zero ceremony, integrates with for loops, comprehensions, and standard-library functions (map/filter/itertools) for free |
Only works for in-process, synchronous (or the language's native async-iterator variant) traversal — doesn't itself solve pagination against a remote source |
| Hand-rolled iterator object implementing an explicit interface | Full control over traversal state, cursor position, and pause/resume semantics; portable pattern across languages that lack native protocol support | More boilerplate than a generator; easy to get hasNext()/next() state management subtly wrong (off-by-one, double-advance) |
| Direct index-based loop over exposed internals | Simplest possible code for a plain array | Breaks the moment the underlying structure changes (array → tree → remote cursor) — every call site that assumed direct indexing has to be rewritten, and the internal representation is no longer free to change |
The realistic decision in most modern backend code isn't "iterator pattern vs. no iterator pattern" — the language already gives you one — it's "does this traversal fit the language's built-in synchronous iterator, or does it need an async iterator / generator because each next() involves an I/O call (a paginated API, a cursor-based DB query)." That second case is where explicitly reaching for the pattern (an async generator, or a class wrapping a page-fetching cursor) still earns its keep in 2026 codebases.
When to use / when not to
- Use whenever traversal needs to be decoupled from a collection's internal shape — consuming code should be able to iterate a list, a tree, or a paginated remote resource through the same loop syntax.
- Reach for a custom iterator (usually a generator, not a hand-rolled class) specifically when each "next element" requires work beyond an array bump — fetching the next page of a REST/GraphQL response, streaming rows from a database cursor, walking a tree that has no natural flat representation.
- Don't hand-roll an iterator class in a language with native generator/iterator support for a simple in-memory collection — use the language's built-in protocol; a custom class only adds ceremony there.
- Don't reach for it when the caller genuinely needs random access (index-based lookup, slicing) rather than sequential traversal — Iterator is a sequential-access abstraction, and forcing it onto code that needs
list[500]directly just adds friction.
Common pitfall
Implementing cursor-based pagination as manual offset/limit math scattered across call sites — offset += pageSize repeated wherever a list is consumed — instead of wrapping it in a proper (async) iterator or generator once, at the data-access layer. The manual version tends to duplicate off-by-one bugs across every call site, breaks silently when the underlying sort order isn't stable (offset pagination skips or repeats rows under concurrent writes), and makes it impossible to swap the pagination strategy (offset → cursor-based) without touching every consumer. A single async generator or iterator class that owns the "how do I get the next page" logic means callers just write for await (const item of source) and the pagination strategy can change underneath them without any call-site changes.
Engineering Lens
The pattern's real value shows up as a codebase evolves: when traversal logic is trapped behind a proper iterator, changing the underlying data source — swapping offset pagination for cursor-based pagination, or replacing an in-memory list with a database-backed generator — is a one-file change. When it isn't, that same change means auditing every place that assumed direct index access, which is exactly the kind of refactor that gets deferred indefinitely because it's too risky to touch broadly. This is the same "decouple interface from implementation" instinct that shows up throughout the GoF catalog (Iterator, Strategy, Bridge all do a version of it) — the specific value here is that it's the one instance nearly every language now bakes directly into its core syntax, which is itself a signal of how consistently useful the decoupling turned out to be in practice.
Related
- Command Pattern — both wrap an operation (traversal step vs. action) behind a small uniform interface to decouple caller from implementation detail