Home › Low-Level Design › Iterator Pattern
I

Iterator Pattern

Give me a way to walk through a collection without knowing how it stores its stuff. That one sentence is the whole pattern. Everything else is plumbing. The people who nail this in interviews stop thinking about for loops and start thinking about who owns the cursor. Weak answers describe syntax. Strong answers name the boundary the pattern defends: traversal on one side, storage on the other.

Low-Level DesignEasy9 min read

If you remember five things

The whole pattern in one glance. Everything below defends these five calls.

  1. Intent. Walk a collection element by element without exposing how it is stored (array, tree, linked list, remote pages).
  2. Two methods. hasNext() asks "anything left?" and next() hands over the next item and advances the cursor. That is the entire surface.
  3. Cursor lives in the iterator, not the collection. That is why two loops over the same collection do not stomp on each other.
  4. Program to the interface. The caller depends on Iterator, never on PlaylistIterator, so you can swap in a shuffled or reversed walk with zero caller changes.
  5. The classic gotcha. Mutating the collection mid-iteration. Either snapshot a copy or fail fast, never silently return garbage.

1The problem it solves#

Start with the pain, because the pattern only makes sense once you feel it.

  • Say I hand you a Playlist. Internally it might be an array, a linked list, or a tree sorted by play count.
  • You want to loop over the songs. How?
  • The naive answer: expose the internal list. Add a getSongs() that returns the raw ArrayList, and the caller writes a normal for loop.

That looks fine for about ten seconds. Here is what it actually costs:

  • Storage leaks into the caller. They now know I use an ArrayList. Switch to a tree tomorrow and their code breaks. One storage change, N call sites to fix.
  • Your internals are mutable from the outside. Anyone can grab that list and call .clear(). You handed out your private state.
  • Every collection becomes its own dialect. Three collections stored three ways (songs, playlists, podcasts) means the caller learns three traversal styles.

The deeper problem is that two jobs got tangled together.

  • Traversal logic and storage logic are now mixed.
  • That is an encapsulation leak, and it breaks the Single Responsibility Principle: the caller is suddenly responsible for both reading data and knowing the layout.
  • I want to let people walk my collection without telling them how it is built, and without letting them poke at my guts.

That is the gap. The Iterator pattern fills it.

The one-line intent

Separate "how you move through a collection" from "how the collection stores things." Say that sentence out loud before you draw anything, and the rest of the interview is just you proving you meant it.

2The idea (mental model)#

Here is how I explain it at the whiteboard.

  • A collection is a book. An iterator is a bookmark.
  • The book does not know where you are in it. The bookmark does.
  • Two bookmarks can sit in the same book on different pages, and neither confuses the other.

So instead of the caller managing an index, I hand them a small object whose entire job is "remember where we are and give me the next item." Two methods:

  • hasNext(): is there anything left?
  • next(): give me the next thing, and move the bookmark forward.

What that buys you:

  • The caller never sees the array, the index, or the node pointers.
  • They just keep asking "got more?" and "give me the next one" until the answer is no.
  • The collection stays sealed. The traversal state lives in a separate, throwaway object.

Storage is the collection's business. Position is the iterator's business. Mix them and the pattern is gone.

3Structure#

There are four roles, and naming them cleanly is half the interview.

  • Iterator (interface): declares hasNext() and next().
  • ConcreteIterator: implements it and holds the cursor state for one specific collection.
  • IterableCollection (the aggregate, an interface): declares something like createIterator().
  • ConcreteCollection: implements createIterator() and knows how to build the right iterator for its own layout.

Why a separate object and not a method on the collection:

  • The collection hands out a fresh iterator every time.
  • Each one tracks its own position, so there is no shared state and no bug when two parts of your code iterate the same collection.
  • This is programming to an interface, not an implementation: the caller depends on Iterator, never on PlaylistIterator.
classDiagram class Iterator~T~ { <> +hasNext() boolean +next() T } class IterableCollection~T~ { <> +createIterator() Iterator } class Playlist { -List songs +add(song) void +createIterator() Iterator } class PlaylistIterator { -List songs -int position +hasNext() boolean +next() Song } Iterator <|.. PlaylistIterator IterableCollection <|.. Playlist Playlist --> PlaylistIterator Playlist *-- Song
Notice the direction of the arrows. Playlist creates the PlaylistIterator (solid association), but the caller only ever talks to the Iterator interface (dashed "implements"). The concrete iterator type never escapes into client code.

4Implementation in Java#

I will build the playlist, kept tight so the design shows through.

public interface Iterator<T> {
    boolean hasNext();
    T next();
}

public interface IterableCollection<T> {
    Iterator<T> createIterator();
}

The collection. The internal storage is an ArrayList, and nobody outside gets to see it.

public class Playlist implements IterableCollection<Song> {
    private final List<Song> songs = new ArrayList<>();

    public void add(Song song) {
        songs.add(song);
    }

    @Override
    public Iterator<Song> createIterator() {
        return new PlaylistIterator(songs);
    }
}

The iterator. This is the object that holds the cursor, and the only object that does.

public class PlaylistIterator implements Iterator<Song> {
    private final List<Song> songs;
    private int position = 0;

    public PlaylistIterator(List<Song> songs) {
        this.songs = songs;
    }

    @Override
    public boolean hasNext() {
        return position < songs.size();
    }

    @Override
    public Song next() {
        if (!hasNext()) {
            throw new NoSuchElementException("No more songs");
        }
        return songs.get(position++);
    }
}

The caller. Clean, and completely blind to how Playlist stores anything.

Playlist playlist = new Playlist();
playlist.add(new Song("Midnight City"));
playlist.add(new Song("Teardrop"));

Iterator<Song> it = playlist.createIterator();
while (it.hasNext()) {
    Song song = it.next();
    System.out.println(song.getTitle());
}
Pause before you read on: I decide Playlist should play in shuffled order. How much of that while loop do I have to rewrite?
Reveal the answer
Not one character. I write a ShuffledIterator that walks a randomized order and return it from createIterator(). The caller still depends only on the Iterator interface, so the loop body is untouched. That is the Open/Closed Principle in action: new traversal behavior added, zero caller code modified. If you can say that sentence, you understand why the iterator is a separate object.

5A real-world example#

You have used this a thousand times. Java's java.util.Iterator is exactly this pattern.

  • Every enhanced for loop is sugar over it:
for (Song song : playlist) { ... }
  • The compiler calls iterator() under the hood and drives hasNext() and next() for you.
  • That is why you loop over an ArrayList, a HashSet, and a LinkedList with identical syntax even though their internals are wildly different (array, hash buckets, node pointers). One loop body, three storage layouts.

Where it earns its keep in real systems:

  • Paginated API clients. You write for (Record r : client.allRecords()) while the iterator quietly fetches page 2 only after you exhaust page 1. Lazy loading hides inside next() and the caller never thinks about pages.
  • Database result sets. A ResultSet cursor is an iterator over rows you have not loaded yet. Stream a million rows without a million rows sitting in heap.
  • Tree and graph traversal. In a file-system explorer you can expose a DepthFirstIterator and a BreadthFirstIterator over the same directory tree. Same collection, two walk orders, same loop body.
From the interviewer's chair

That last one is the move I wait for. Ask me to design a file explorer or an org-chart browser, then offer two iterators over one structure, and I know you actually understand the separation instead of reciting the GoF book. Reaching for it unprompted is worth more than a perfect class diagram.

6When to use it, and when not to#

Reach for it when:

  • You want to hide how a collection is stored but still let people walk it.
  • You need more than one way to traverse the same structure: forward, reverse, filtered, shuffled.
  • You are building a custom collection and want it to feel native, so make it Iterable and it drops into a for-each.
  • Traversal should be lazy, computing or fetching the next item only when asked.

Skip it when:

  • You are working with a plain List or Map that already gives you iteration. Do not wrap ArrayList in your own iterator class to look clever. The standard library already solved this.
  • The collection is tiny, private, and only you touch it. A direct index loop is fine.

Rule of thumb I use:

  • If the collection crosses a module or API boundary, an iterator is probably worth it.
  • If it stays inside one class, usually not. The pattern is about boundaries, and with no boundary there is no payoff.

7Common mistakes#

Calling next() without checking hasNext().

  • You blow past the end and either get an exception or, worse, silently return garbage.
  • Always guard next() so it throws clearly when empty.
  • That is why the example throws a NoSuchElementException with a message, on purpose.

Leaking the collection through the iterator (the classic gotcha).

  • PlaylistIterator holds a reference to the same songs list.
  • Add items to the playlist mid-iteration and behavior gets weird.
  • Java's built-in iterators defend this with fail-fast checks that throw ConcurrentModificationException.
  • For an interview, name the two fixes: snapshot a copy at createIterator() time, or track a modification count and detect the change. Do not ignore it.

Putting traversal state on the collection instead of the iterator.

  • The anti-pattern: a private int currentIndex on Playlist itself, with first() and next() methods on the collection.
  • Now you can only have one traversal at a time, and two loops over the same collection stomp on each other.
  • The whole point is that state lives in the iterator, so each loop gets its own cursor.

Making the iterator do too much.

  • It walks and returns. That is it.
  • It should not filter, sort, and transform all at once unless that is literally its named job.
  • Keep hasNext and next simple.

Forgetting to implement Iterable in Java.

  • If you build a custom collection, implement java.lang.Iterable so it works with for-each.
  • Rolling your own createIterator() when the language hands you iterator() for free just means your collection will not play nicely with the rest of Java.
Red flag that fails you here

Putting the cursor on the collection, then freezing when I ask "what happens if two parts of my code loop over this at the same time?" If your Playlist owns currentIndex, the honest answer is "they corrupt each other," and that admission tells me you have not actually used this pattern. State up front that position lives in the iterator, and this trap never opens.

8Where it shows up in interviews#

Iterator rarely shows up as "design an iterator." It hides inside a bigger problem, and the interviewer watches whether you reach for it naturally.

  • "Design an in-memory collection or custom list." The follow-up is always "how do people loop over it?" The right answer is make it Iterable.
  • "Design a file system." Offer different traversal orders as iterators. That earns points.
  • "Design a card game or deck." Dealing cards one at a time is basically next().
  • As a building block in other patterns. Composite pairs with it constantly (you iterate a tree of nodes). Observer sometimes iterates its subscribers.

If you are asked directly, run this order:

  • Start with the mental model: the collection is the book, the iterator is the bookmark.
  • Write the two interfaces, then the concrete pair.
  • State up front that traversal state lives in a separate object, not the collection, and say why.

That one sentence tells the interviewer you understand the pattern instead of memorizing it. This is an Easy pattern, so the bar is not "can you write it." The bar is "do you know why it exists." Lead with the why.

Follow-ups the interviewer will ask (and your answers)
"What happens if the collection is modified while I am iterating it?"
That is the classic failure. I pick one of two defenses. Snapshot a copy of the backing store inside the iterator at creation time, so later mutations do not affect this walk. Or keep a modification counter on the collection, read it into the iterator at creation, and compare on each next(), throwing ConcurrentModificationException the moment they diverge. Fail fast beats silently returning garbage.
"Why not just put next() and hasNext() directly on the Playlist?"
Because then the collection owns one cursor, and two loops over the same collection corrupt each other. A separate iterator object means each traversal has its own position. It also keeps the collection small: storage is its job, position is the iterator's job.
"How would you support reverse or filtered traversal?"
Add another ConcreteIterator. A ReverseIterator counts down from size() - 1, a FilteredIterator wraps another iterator and skips items that fail a predicate in next(). The collection returns whichever one the caller asks for, and because callers depend on the Iterator interface, nothing else changes. That is the Open/Closed Principle paying off.
"How does this handle lazy or infinite sequences?"
Nicely, because the iterator only produces the next item when next() is called. A paginated client fetches the next page lazily on exhaustion, a number generator computes on demand, and an infinite stream just keeps returning true from hasNext(). Nothing forces the whole collection into memory up front.
"In Java, Iterator versus Iterable: what is the difference?"
Iterable is the collection role: it has one method, iterator(), that hands out a fresh cursor. Iterator is the cursor itself, with hasNext() and next(). You implement Iterable on your collection so for-each works, and each call to iterator() returns a new independent Iterator.

What you can now explain in the room

  • The one-line intent: walk a collection without exposing how it stores its elements.
  • Why the cursor must live in the iterator and not on the collection.
  • The four roles and how they map onto a clean Java `Playlist` plus `PlaylistIterator`.
  • Why programming to the `Iterator` interface lets you swap in shuffled or reversed walks with zero caller changes.
  • The classic gotcha (mutating mid-iteration) and the two fixes: snapshot or fail-fast.
  • When to skip the pattern entirely, and where it quietly powers Java's `for-each`, paginated clients, and result sets.

Next, look at the Composite pattern (iterators walk composite trees beautifully) and the Observer pattern, where you will often iterate a list of subscribers.

Get a prep plan built around your Iterator Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Iterator Pattern prep plan is on the way.