Intent
Provide a way to access the elements of an aggregate object sequentially without exposing the aggregate's underlying representation. The caller asks for the next thing; the iterator handles the bookkeeping.
The Problem
You're integrating a third-party API that returns paginated results — 50 items per page, with a next_cursor token in each response. The first version of your client looks like this:
public function syncCustomers(): void
{
$page = 1;
do {
$response = $this->api->fetch(['page' => $page]);
foreach ($response['items'] as $row) {
$this->process($row);
}
$page++;
} while ($response['has_more']);
}
Now the next caller wants to do the same thing — and copies the loop. Then a third caller. Then a fourth caller forgets the has_more check and only ever processes the first page. Worse, the API switches from page-based to cursor-based pagination next quarter — and now you have four loops to update.
The shape of the smell: the traversal logic is mixed in with whatever you wanted to do with each item. Every caller relearns how the collection is shaped.
The Solution
Iterator says: wrap the traversal in its own object — an iterator — that exposes a uniform interface (next(), hasNext(), or in modern languages, just for ... of). The caller asks for items one at a time; the iterator handles fetching pages, advancing cursors, and knowing when there's nothing left.
final class CustomerIterator implements \Iterator
{
private int $page = 1;
private int $cursor = 0;
private array $buffer = [];
private bool $hasMore = true;
public function __construct(private CustomerApi $api) {}
public function current(): array { return $this->buffer[$this->cursor]; }
public function key(): int { return ($this->page - 1) * 50 + $this->cursor; }
public function valid(): bool { return $this->cursor < count($this->buffer) || $this->hasMore; }
public function next(): void { /* advance, fetch next page when needed */ }
public function rewind(): void { /* reset to first page */ }
}
Now the caller writes a plain foreach — no pagination logic, no has_more flags, no leaked vendor concepts. When the API changes to cursor-based pagination, you fix the iterator class. The four callers don't move.
Real-World Analogy
A book with a bookmark. The bookmark knows what page you're on. You don't need to know whether the book is hardcover or paperback, whether the spine is glued or stitched, whether the pages are numbered consecutively or split across volumes. You just say "next page" or "previous page," and the bookmark advances.
Two readers can have two bookmarks in the same book and read at different paces — the book doesn't care. That's exactly how iterators work: the traversal state lives in the iterator, not in the collection.
Structure
Four roles you'll see in every Iterator implementation:
- Iterator — the interface for traversing. Usually
hasNext()+next(), or the language's native iteration protocol. - Concrete Iterator — the specific walker. Holds the traversal state (current position, page cursor, etc.). One per traversal order: a tree might have a
DepthFirstIteratorand aBreadthFirstIterator. - Aggregate — the collection. Exposes a way to create an iterator over itself.
- Concrete Aggregate — the specific collection: an array, a tree, a paginated API client, a database cursor.
The defining trick: traversal state lives in the Iterator, not in the Aggregate. Two iterators can walk the same Aggregate independently.
Code Examples
Here's a paginated-API iterator in five languages. Notice how each language has a native iteration protocol — and how implementing it makes the iterator usable with the language's regular for loop.
class CustomerCollection implements Iterable<Customer> {
constructor(private api: CustomerApi) {}
// Implement the language's iteration protocol — JavaScript will use this
// automatically with `for ... of`, spread, destructuring, etc.
async *[Symbol.asyncIterator](): AsyncIterator<Customer> {
let cursor: string | null = null;
do {
const page = await this.api.fetch({ cursor });
for (const customer of page.items) yield customer;
cursor = page.nextCursor;
} while (cursor !== null);
}
}
// Caller — the pagination shape is invisible.
for await (const customer of new CustomerCollection(api)) {
process(customer);
}
class CustomerCollection:
def __init__(self, api):
self._api = api
# Generator function — Python's iteration protocol made trivial.
def __iter__(self):
cursor = None
while True:
page = self._api.fetch(cursor=cursor)
for customer in page["items"]:
yield customer
cursor = page.get("next_cursor")
if not cursor:
break
# Caller:
for customer in CustomerCollection(api):
process(customer)
public final class CustomerCollection implements Iterable<Customer> {
private final CustomerApi api;
public CustomerCollection(CustomerApi api) { this.api = api; }
@Override
public Iterator<Customer> iterator() {
return new Iterator<>() {
private String cursor = null;
private Iterator<Customer> page = Collections.emptyIterator();
private boolean exhausted = false;
@Override
public boolean hasNext() {
while (!page.hasNext() && !exhausted) {
var response = api.fetch(cursor);
page = response.items().iterator();
cursor = response.nextCursor();
if (cursor == null) exhausted = true;
}
return page.hasNext();
}
@Override
public Customer next() { return page.next(); }
};
}
}
// Caller:
// for (Customer c : new CustomerCollection(api)) process(c);
<?php
namespace App\Customers;
final class CustomerCollection implements \IteratorAggregate
{
public function __construct(private CustomerApi $api) {}
// PHP's native iteration protocol — generators make this almost free.
public function getIterator(): \Generator
{
$cursor = null;
do {
$page = $this->api->fetch(['cursor' => $cursor]);
foreach ($page['items'] as $customer) {
yield $customer;
}
$cursor = $page['next_cursor'] ?? null;
} while ($cursor !== null);
}
}
// Caller:
foreach (new CustomerCollection($api) as $customer) {
process($customer);
}
package customers
// Go's idiomatic iterator is a callback-based iterator function. As of Go 1.23
// you can also implement range-over-func iterators directly.
type CustomerCollection struct {
api CustomerApi
}
// Range walks every customer across all pages, yielding to the caller via the
// provided callback. Returning false from the callback stops the iteration.
func (c *CustomerCollection) Range(yield func(Customer) bool) {
cursor := ""
for {
page, err := c.api.Fetch(cursor)
if err != nil {
return
}
for _, customer := range page.Items {
if !yield(customer) {
return
}
}
if page.NextCursor == "" {
return
}
cursor = page.NextCursor
}
}
// Caller:
// (&CustomerCollection{api}).Range(func(c Customer) bool {
// process(c)
// return true
// })
The pagination shape is gone from every caller. Each language hits the same idea through its own machinery — generators in PHP and Python, async iterators in TypeScript, anonymous inner classes in Java, range-over-func in Go.
When to Use It
Reach for Iterator when you can answer "yes" to any of these:
- The collection's internal shape isn't simple or stable. Trees, graphs, paginated APIs, database cursors, lazy/streaming results — anywhere the traversal deserves its own home.
- You want to traverse the same collection multiple ways. A tree gets a
DepthFirstIteratorand aBreadthFirstIterator. A list gets aReverseIterator. The collection stays one class; the iterators multiply. - You want lazy or streaming traversal. Fetch the next page only when needed. Read the next line of a 10 GB log file only when asked. Iterators are the natural shape for "compute on demand."
- Multiple traversals must run concurrently. Two parts of the code walking the same collection at different paces, without interfering. Each gets its own iterator with its own state.
If your collection is just an array and you only ever do for (item of array), you don't need a custom iterator — the array already is one.
Pros and Cons
Pros
- The collection's internal representation stays hidden. Swap the storage from an array to a tree to an API without touching callers.
- The traversal logic lives in one place. Pagination quirks, cursor management, exhaustion checks — all in the iterator class.
- Lazy evaluation is natural — only the items you actually consume get fetched.
- Multiple iterators over the same collection can run independently.
- Modern languages bake iteration into their core syntax (
for...of,foreach,for ... range), so the usage costs almost nothing.
Cons
- Custom iterators add a class. For trivial collections, just exposing the underlying array is simpler.
- Iterators are stateful. Sharing one between threads or async contexts without coordination is a foot-gun.
- "External" iterators (caller drives
next()) and "internal" iterators (collection accepts a callback) have different ergonomics — pick the one your language prefers. - Mutating the collection while iterating it causes well-known bugs in every language. Document the policy.
Pro Tips
- Prefer the language's native protocol over hand-rolled
Iterator/hasNext. PHP'sGenerator, Python'syield, JavaScript'sSymbol.asyncIterator, Java'sIterable, Go's range-over-func — they all integrate with the language'sforloops and ecosystems for free. - One iterator per traversal order. Don't make a god iterator with a
modeparameter.DepthFirstIteratorandBreadthFirstIteratorare two clean classes; oneTreeIterator($mode)is one mess. - Reset is its own operation. If callers might want to rewind, expose
reset()explicitly. Don't try to makenext()cycle. - Decide your fail-fast policy early. What happens if the underlying collection mutates while iteration is in progress? Pick a behavior — throw, snapshot, or silently skip — and document it.
- For paginated APIs, surface backpressure. If consuming faster than the API can serve, the iterator should slow down (await, backoff) rather than buffer indefinitely.
Relations with Other Patterns
- Composite is the classic shape Iterator is built to traverse. Tree-shaped data + a uniform "walk the children" iterator = a clean, recursive design.
- Visitor is the alternative when you want to do something different at each node of a Composite. Iterator hands you items uniformly; Visitor dispatches per node type.
- Memento — sometimes used to capture an iterator's position so you can rewind to it later. Iterator's
next()advances; Memento can save and restore the state. - Chain of Responsibility has a passing-along shape similar to Iterator's "advance" — but in CoR each handler decides whether to handle or pass; in Iterator the caller decides what to do with each item.
Final Tips
The iterator pattern is the one you've used a thousand times before reading about it. Every for loop in a modern language is iteration in disguise; what the pattern teaches is how to expose your own collections — your tree, your stream, your paginated client — as something for can consume.
The discipline is restraint: when you hand-roll an iterator, do as little as possible inside it. Walking is the iterator's whole job. The moment it grows logic beyond "advance and yield," you've conflated traversal with processing — and what you really wanted was a Decorator wrapping it, or a Visitor operating across it.


