Free Interactive Course · Design Patterns

Iterator Design Pattern

Walk a collection without knowing how it stores anything — so the loop survives the storage changing underneath it.

Behavioural Patternseasya.k.a. Cursora.k.a. Enumerator
ShareXLinkedIn
In one sentence

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

01

The problem Iterator solves

The problem

A playlist is an array, so the code that plays it uses an index: for (int i = 0; i < playlist.size(); i++). Forty places in the app do the same, plus the shuffle screen, the export, the duration total and the search.

Then playlists get very long and you switch to a linked list for cheap insertion. Every index-based loop is now quadratic — each get(i) walks from the head. Nothing breaks; it just gets slower until someone profiles it.

Then playlists gain folders, so the structure is a tree. Now every loop needs recursion. Then playlists move to the server and arrive one page at a time, so every loop needs to know about page tokens and network errors.

Four storage changes, and each one edited forty pieces of code that never cared how tracks were stored. All they ever wanted was "give me the next track".

02

How the Iterator pattern works

Put the walking logic inside a small object that belongs to the collection, and let callers ask for one:

  1. The collection exposes one method — iterator() — that returns a fresh cursor over its contents.
  2. The iterator holds the position, and knows the structure: an array index, a node pointer, a stack of tree nodes, a page token and a buffer.
  3. The client uses two operations, hasNext() and next(), and learns nothing else. The loop is identical for all four storage designs.
  4. Because position lives in the iterator rather than the collection, several walks can run at once over the same data without interfering.
You almost never write one by hand any more — and that's worth understanding rather than skipping. Every language here has absorbed the pattern into syntax: for-each, for…of, range, foreach. What they've absorbed is the interface; what you still have to decide is everything interesting — lazy or eager, what happens if the collection changes mid-walk, whether the walk can fail (a network page), and whether the caller can stop early. Those decisions haven't gone anywhere, and getting them wrong still produces the same bugs it did in 1994.
PlayerScreenone loop, forever«interface» IteratorhasNext() · next()ArrayIteratorLinkedIteratorTreeIteratorPagedApiIteratorwhile (it.hasNext()) play(it.next());the line above is the same for an array, a tree and a paginated HTTP endpoint
Participants. The Aggregate creates an Iterator; the Concrete Iterator holds the position and knows the structure; the Client knows only hasNext and next. Keeping the position in the iterator — not in the collection — is what allows two walks over the same data at the same time.
03

See it: swap the storage, keep the loop

The loop at the top never changes. Switch the storage underneath it and step through — the array walks by index, the linked list follows pointers, the tree pushes and pops a stack, and the paginated API fetches a page when the buffer runs dry. The caller cannot tell which.

▶ Try it — one loop, four structures

An interactive iterator: the same while (it.hasNext()) loop walks an array by index, a linked list by pointer, a folder tree with a stack, and a paginated HTTP endpoint that fetches the next page when its buffer empties — with identical calling code in all four cases.

The paginated variant is the one that pays for the whole pattern. Its next() sometimes makes an HTTP request — and the loop still reads like a loop over an array. That's the power and the danger together: hiding the representation also hides its cost, which is why "why is this endpoint slow?" so often ends at an innocent-looking for.
04

Iterator pattern code examples

The hand-written form, and then the language feature that replaced it in each case.

public final class Playlist implements Iterable<Track> {

    private final Node head;

    @Override public Iterator<Track> iterator() {
        return new Iterator<>() {
            private Node cursor = head;      // position lives HERE, not in Playlist

            @Override public boolean hasNext() { return cursor != null; }

            @Override public Track next() {
                if (cursor == null) throw new NoSuchElementException();
                Track track = cursor.track;
                cursor = cursor.next;
                return track;
            }
        };
    }
}

// Implementing Iterable is what earns you the for-each syntax:
for (Track track : playlist) {
    play(track);
}

// The JDK's collections are FAIL-FAST, and it's worth knowing why. Each
// iterator remembers the collection's modCount when it was created; if the
// collection is structurally modified while you're walking it, next() throws
// ConcurrentModificationException:
//
//     for (Track t : playlist)
//         if (t.isExplicit()) playlist.remove(t);   // throws
//
// Use it.remove(), removeIf(), or collect and remove afterwards. Note the name
// is misleading — a single thread triggers it just as easily as two.
Read across the tabs: five of the seven let you write an iterator as a generator — yield turns local variables into the cursor and deletes the state machine entirely. Go held out longest and then adopted the same idea in 1.23 as iter.Seq. What each language makes explicit differs, and that's where the real content is: C++ specifies iterator categories and exactly when iterators are invalidated; Java throws ConcurrentModificationException for the same situation; C# and JavaScript expose laziness so plainly that enumerating twice quietly does the work twice; and Go's iter.Seq2[T, error] refuses to let a walk that can fail pretend otherwise.
05

How to implement Iterator

  1. Use your language's built-in protocol — Iterable, __iter__, Symbol.iterator, begin/end, iter.Seq — so callers get the native loop syntax for free.
  2. Write it as a generator if you can. It removes the position bookkeeping that hand-written iterators get wrong.
  3. Keep the position in the iterator, never in the collection, so two walks can run at once.
  4. Decide and document what happens if the collection changes mid-walk: fail fast, snapshot, or tolerate. Silence here is a bug waiting for a busy day.
  5. Make it lazy when the source is large or remote, and make sure early termination actually stops the work rather than draining the source first.
  6. If the walk can fail — network, disk, parse — put that in the signature. A next() that throws from inside an innocent-looking loop is hard to reason about.
  7. Expose the shape callers need: a tree may deserve depth-first and breadth-first iterators rather than one that guesses.
06

When to use Iterator — and when not to

Use it to give access to a collection's contents without exposing its representation; to support more than one kind of traversal over the same structure; to give several collections a uniform walking interface; and to stream something too large or too remote to materialise.

Where it goes wrong

Modifying while iterating. The classic. Java throws ConcurrentModificationException, C# invalidates the enumerator, C++ gives you undefined behaviour and a good day debugging. Remove through the iterator, use a bulk operation, or iterate a copy.

Hidden cost behind a friendly loop. A for over a lazy source can issue a database query or an HTTP request per iteration. The pattern hides the representation, which means it also hides the bill — this is the same N+1 problem lazy proxies cause, arriving through a different door.

Enumerating twice. With deferred execution (LINQ, generators), walking the same query a second time re-runs it. Materialise once when the source is expensive — and don't materialise at all when it's infinite.

Spreading a lazy source. [...iterable] and list(gen) drain everything, which turns a carefully lazy pipeline into a full load, or hangs forever on an endless one.

Writing one by hand for no reason. If the collection is a wrapper around a list, return the list's own iterator. A hand-rolled cursor with an off-by-one is a poor trade for a method call you didn't need.

You want to…UseBecause
Walk a collection without exposing how it stores thingsIteratorThe cursor knows the structure; the caller knows two methods.
Walk a tree of parts and wholes uniformlyCompositeComposite defines the tree; an iterator flattens it into a sequence.
Run a new operation over every node of a structureVisitorIterator gives you the elements; Visitor gives you type-aware behaviour per element.
Produce elements lazily from a generator functionIterator (built in)Generators are the pattern absorbed into the language.
Give a whole subsystem one simple entry pointFacadeDifferent problem — Facade simplifies an API, Iterator sequences data.
07

Quick check

🧠 Quick check
You loop over a list and call list.remove(item) inside the loop when a condition matches. What happens in Java?

In the wild

Javajava.util.Iterator and Iterable behind every for-each loop; the fail-fast ConcurrentModificationException is the pattern defending itself.
PythonThe iterator protocol (__iter__/__next__), generators, and the whole of itertools — lazy pipelines built out of nothing but iterators.
C#IEnumerable<T>, yield return, and LINQ's deferred execution; IAsyncEnumerable<T> with await foreach for paged sources.
C++begin()/end(), the iterator category hierarchy that STL algorithms select on, and C++20 ranges and views.
JavaScriptSymbol.iterator, generators, for…of, spread — and Symbol.asyncIterator with for await for streams and paged APIs.
Goiter.Seq and iter.Seq2 (Go 1.23) turning a yield-callback into a range-able value; bufio.Scanner is the older hand-written form.

Frequently asked questions

Is the Iterator pattern still relevant if my language has for-each?
The interface is built in; the design decisions are not. Every for-each is this pattern — but you still choose whether your iterator is lazy or eager, what happens when the collection changes mid-walk, whether early exit stops the underlying work, and whether a walk that can fail says so in its type. Those are the parts that cause production bugs, and no amount of syntax sugar decides them for you.
What is the difference between an internal and an external iterator?
An external iterator is controlled by the client: you call next(), so you can stop, pause, interleave two walks, or hand the cursor to someone else. An internal iterator is controlled by the collection: you pass in a function and it calls you for each element (forEach, Go's pre-1.23 Each). Internal is simpler to write and harder to abandon halfway; external is more flexible and more code. Generators are the compromise that mostly won — they look internal to write and behave externally to use.
Why can't I modify a collection while iterating over it?
Because the iterator holds a position into a structure you're changing underneath it — removing an element shifts everything after it, and inserting can force an array to reallocate entirely. Languages differ only in how kindly they tell you: Java and C# fail fast with an exception, Python may silently skip elements when you mutate a list mid-loop, and C++ calls it undefined behaviour. The fixes are the same everywhere: remove through the iterator, use a bulk operation like removeIf, or iterate over a copy.
Are generators the same as the Iterator pattern?
They're the pattern with the boilerplate deleted. A generator's local variables and instruction pointer are the cursor state, so yield gives you hasNext/next semantics without a class, and laziness comes free. Python, JavaScript, C# and now Go all provide them, and they are the right default. A hand-written iterator class still earns its place when the cursor needs to be inspected, reset, copied or serialised — a generator's state is deliberately opaque.
How do I iterate something that arrives one page at a time?
With an async iterator, and be explicit that it is one — IAsyncEnumerable and await foreach, Symbol.asyncIterator and for await, an async generator in Python, or iter.Seq2[T, error] in Go. The iterator buffers a page and fetches the next only when the buffer empties, so the caller writes an ordinary loop. Two rules matter: don't materialise the whole thing ([...it] defeats the point), and make sure breaking out of the loop actually stops the fetching.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.