Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work
Iterator
Walk through the items of a collection one at a time without knowing how it's stored: an array, a tree, a database cursor, or pages from an API.
The problem
A reporting job needs every user from an API that returns 100 users per page. The first version loads all pages into one big list before processing. With 2 million users, it runs out of memory, and it can't start work until every page has downloaded.
Each caller also re-implements the paging logic ("is there a next page? what's the cursor?") in its own loop.
Real-world analogy
A TV remote's "next channel" button. You don't need to know how channels are stored or numbered; you press "next" until you've seen what you want. The remote (iterator) keeps track of where you are.
Without the pattern
users, page = [], 1
while True:
batch = api.get_users(page) # paging logic copied into every caller
if not batch:
break
users.extend(batch) # everything held in memory at once
page += 1
for user in users:
process(user)
The pattern
- Iterator: an object that knows the current position and gives the next item (
next()), and says when there are no more. - Iterable collection: creates iterators (
__iter__,[Symbol.iterator],iterator(),getIterator()). - Client: uses the language's loop (
for ... in,for ... of, enhancedfor,foreach) and never sees how items are stored or fetched.
Every modern language builds this pattern into its loops, and generators (yield) make writing iterators easy: the function pauses after each item and resumes when the next one is requested.
The code
Pages are fetched only when the loop needs them. The loop stops early after finding the user it wants, so page 3 is never fetched.
PAGES = {1: ["ada", "bob"], 2: ["cy", "dee"], 3: ["eve", "fay"]}
def fetch_page(n): # pretend this is an HTTP call
print(f" (fetching page {n})")
return PAGES.get(n, [])
class PagedUsers: # iterable: hides paging behind a normal loop
def __iter__(self):
page = 1
while True:
batch = fetch_page(page)
if not batch:
return
yield from batch # a generator: pauses after each user
page += 1
for user in PagedUsers():
print("processing", user)
if user == "dee":
print("found dee, stopping early")
break
Output
(fetching page 1) processing ada processing bob (fetching page 2) processing cy processing dee found dee, stopping early
const PAGES: Record<number, string[]> = { 1: ["ada", "bob"], 2: ["cy", "dee"], 3: ["eve", "fay"] };
function fetchPage(n: number): string[] { // pretend this is an HTTP call
console.log(` (fetching page ${n})`);
return PAGES[n] ?? [];
}
class PagedUsers implements Iterable<string> { // iterable: hides paging behind a normal loop
*[Symbol.iterator](): Iterator<string> { // a generator: pauses after each user
for (let page = 1; ; page++) {
const batch = fetchPage(page);
if (batch.length === 0) return;
yield* batch;
}
}
}
for (const user of new PagedUsers()) {
console.log("processing", user);
if (user === "dee") {
console.log("found dee, stopping early");
break;
}
}
Output
(fetching page 1) processing ada processing bob (fetching page 2) processing cy processing dee found dee, stopping early
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
public class Main {
static final Map<Integer, List<String>> PAGES =
Map.of(1, List.of("ada", "bob"), 2, List.of("cy", "dee"), 3, List.of("eve", "fay"));
static List<String> fetchPage(int n) { // pretend this is an HTTP call
System.out.println(" (fetching page " + n + ")");
return PAGES.getOrDefault(n, List.of());
}
public static void main(String[] args) {
for (String user : new PagedUsers()) {
System.out.println("processing " + user);
if (user.equals("dee")) {
System.out.println("found dee, stopping early");
break;
}
}
}
}
class PagedUsers implements Iterable<String> { // iterable: hides paging behind a normal loop
public Iterator<String> iterator() {
return new Iterator<>() {
private int page = 0;
private Iterator<String> batch = List.<String>of().iterator();
public boolean hasNext() {
while (!batch.hasNext()) { // fetch the next page only when needed
List<String> next = Main.fetchPage(++page);
if (next.isEmpty()) return false;
batch = next.iterator();
}
return true;
}
public String next() {
if (!hasNext()) throw new NoSuchElementException();
return batch.next();
}
};
}
}
Output
(fetching page 1) processing ada processing bob (fetching page 2) processing cy processing dee found dee, stopping early
Run this on your computer — see “Get Started” for setup.
<?php
const PAGES = [1 => ["ada", "bob"], 2 => ["cy", "dee"], 3 => ["eve", "fay"]];
function fetchPage(int $n): array { // pretend this is an HTTP call
echo " (fetching page $n)\n";
return PAGES[$n] ?? [];
}
class PagedUsers implements IteratorAggregate { // iterable: hides paging behind a normal loop
public function getIterator(): Generator { // a generator: pauses after each user
for ($page = 1; ; $page++) {
$batch = fetchPage($page);
if (!$batch) return;
yield from $batch;
}
}
}
foreach (new PagedUsers() as $user) {
echo "processing $user\n";
if ($user === "dee") {
echo "found dee, stopping early\n";
break;
}
}
Output
(fetching page 1) processing ada processing bob (fetching page 2) processing cy processing dee found dee, stopping early
The caller wrote an ordinary loop. Paging, cursors and fetching are hidden in the iterator, and only two of the three pages were ever requested.
When to use it
- A collection has a complex internal structure (tree, graph, paged API, database cursor, file stream) and callers just want "each item".
- You want lazy processing of large or infinite sequences: items produced only as they're consumed.
- You want several ways to traverse the same collection (depth-first vs breadth-first, filtered, reversed).
When not to use it
- A plain list or array is enough. Every language already iterates those.
- You need random access or the total count up front. Lazy iterators may not know the length until the end.
- Be careful modifying a collection while iterating over it. Many iterators fail or skip items (Java throws
ConcurrentModificationException).
You've already used it
- Every
forloop in Python, JavaScript, Java and PHP uses the iterator protocol:__iter__/__next__,Symbol.iterator,java.util.Iterator, PHP'sIterator/IteratorAggregate. - Generators: Python's
yield, JavaScript'sfunction*, PHP'syield. - Database cursors and Laravel's
lazy()/cursor()stream rows instead of loading them all. - Node.js streams and async iterators (
for await ... of) for files and HTTP responses.
Key points
- Iterator gives sequential access to items without exposing how they're stored.
- It enables lazy processing: fetch or compute items only when needed.
- All four languages build it into their loops; generators make custom iterators short.
- Further reading: Refactoring Guru: Iterator, PHP's Iterator interface and Java's Iterator.
Practice questions
What does the Iterator pattern give you over exposing a collection's internals?
Answer
Clients can loop over items with a standard interface without knowing whether the data is an array, a tree, a database cursor or a paged API, and the collection can change its storage without breaking callers.
How do generators relate to the Iterator pattern?
Answer
A generator function (yield in Python and PHP, function* in JavaScript) is a concise way to write an iterator: it pauses after producing each item and resumes when the next one is requested, keeping its position automatically.
Why is lazy iteration useful for large datasets?
Answer
Items are produced only when the consumer asks for them, so memory stays small (one page or row at a time), work can start immediately, and if the consumer stops early the rest is never fetched.
Open in the app to mark lessons complete, take notes and track your progress.