Last Updated: February 23, 2026
The Iterator Design Pattern is a behavioral pattern that provides a standard way to access elements of a collection sequentially without exposing its internal structure.
At its core, the Iterator pattern is about separating the logic of how you move through a collection from the collection itself. Instead of letting clients directly access internal arrays, lists, or other data structures, the collection provides an iterator object that handles traversal.
It’s particularly useful in situations where:
Let’s walk through a real-world example to see how we can apply the Iterator Pattern to build a more maintainable, extensible, and standardized approach to traversing collections.
Imagine you are building a music streaming application. Users can create playlists, add songs, and play them in various ways. A playlist might contain hundreds of songs, and the player needs to iterate through them one by one.
Your first implementation might look like this:
And your music player might use it like this:
This looks clean enough. The player gets the list of songs and iterates through them. What could go wrong?
As the application grows, several issues emerge:
By returning the internal list, you allow clients to do more than just read. They can add songs, remove songs, clear the list, or even replace it entirely. Nothing prevents a client from calling playlist.getSongs().clear() and wiping out the entire playlist.
Your player assumes the playlist uses a List. What if you decide to change the internal structure? Perhaps you want to store songs in a database and load them lazily. Or maybe you want to use a Set to prevent duplicates.
Every change to the internal structure ripples through all client code.
What if you need to play songs in reverse order? Or shuffle them? Or skip songs that the user has marked as disliked?
Each of these requires writing new loop logic in the client. The playlist has no control over how its contents are accessed.
If your player directly accesses the list, testing the player in isolation becomes harder. You cannot easily mock or stub the playlist's behavior.
We need a way for clients to traverse the playlist that:
This is exactly what the Iterator Pattern provides.
The Iterator Pattern defines a separate object, the iterator, that encapsulates the details of traversing a collection. Instead of exposing its internal structure, the collection provides an iterator that clients use to access elements sequentially.
Two characteristics define the pattern:
createIterator() returns a new, independent iterator with its own position. Multiple clients can traverse the same collection simultaneously without interfering with each other.This separation means you can change how elements are stored without affecting how they are traversed, and vice versa.
Consider a TV remote control. When you press the "next channel" button, you do not need to know how the TV internally organizes its channel list. Maybe it is stored as an array, a linked list, or fetched from a satellite signal.
The remote provides a simple interface: next channel, previous channel. The complexity of channel management is hidden behind that interface.
The Iterator pattern works the same way. The iterator is like the remote control, providing a simple interface to move through a collection without exposing how that collection is structured internally.
The Iterator pattern involves four key components:
Declares the operations required to traverse a collection. At minimum, this includes hasNext() to check if more elements exist, and next() to retrieve the next element.
Implements the Iterator interface for a specific collection. It maintains the current position within the collection and knows how to move to the next element.
Declares a method for creating an iterator. Any class implementing this interface promises it can be iterated.
Implements the IterableCollection interface. It stores elements and returns an appropriate iterator when asked.
You might wonder why we need a separate iterator object. Why not just add hasNext() and next() methods directly to the collection?
The answer lies in supporting multiple simultaneous traversals. If the collection itself tracks the current position, you can only have one traversal at a time. But with separate iterator objects, you can have multiple iterators traversing the same collection independently.
This becomes important in multi-threaded applications or when you need to compare elements at different positions in the same collection.
The Iterator workflow has five steps:
Step 1: The client asks the collection for an iterator by calling createIterator().
Step 2: The collection creates a new iterator object, passing itself (or its data) to the iterator's constructor.
Step 3: The iterator initializes its internal position to the beginning of the collection.
Step 4: The client uses the iterator in a loop: call hasNext() to check for more elements, then next() to get the current element and advance.
Step 5: When hasNext() returns false, traversal is complete. The client can discard the iterator, or the collection can create a new one for another traversal.
Let us refactor our music playlist using the Iterator pattern. We will build the implementation step by step: define the interfaces, implement the collection, implement the iterator, and wire them together.
This interface declares the standard operations for traversing any collection:
The interface is generic (where the language supports it), allowing it to work with any element type. Two methods are sufficient for basic iteration:
hasNext() returns true if there are more elements to iteratenext() returns the current element and advances to the next positionSome iterator interfaces include additional methods like remove(), reset(), or current(). We are keeping it minimal here. You can always extend the interface based on your needs, but starting simple reduces complexity.
This interface ensures that any collection can provide an iterator:
Any class implementing this interface promises to provide an iterator for traversing its elements.
Now we implement the Playlist class. Notice that it no longer exposes its internal list. Instead, it provides controlled access methods that the iterator will use:
The key change: getSongs() is gone. Clients cannot get the raw list anymore. Instead, getSongAt() and getSize() provide the minimum access the iterator needs, while keeping the internal structure private.
The iterator maintains its position and knows how to traverse the playlist:
The iterator is simple by design. It holds a reference to the playlist and an index that starts at zero. Each call to next() returns the current song and advances the index. Each call to hasNext() checks whether the index has reached the end.
The client can now iterate through a playlist without knowing how it's implemented internally.
The client code is clean and focused. It does not know or care whether the playlist uses an ArrayList, LinkedList, or any other structure internally.
Let us evaluate what the Iterator Pattern has given us:
The internal list is no longer exposed. Clients cannot accidentally (or intentionally) modify the playlist's contents through the iterator. The playlist maintains full control over its data.
The client code works with the Iterator interface. If we later change the playlist to use a LinkedList, a database, or a streaming buffer, the client code remains unchanged. We only need to update the iterator implementation.
The Playlist class focuses on managing songs. The PlaylistIterator class focuses on traversal logic. Each class has one reason to change.
Each call to createIterator() returns a new, independent iterator. Multiple parts of your application can traverse the same playlist simultaneously without interfering with each other.
We can now easily add new types of iterators (reverse, shuffled, filtered) without modifying the Playlist class or existing client code.
One of the most powerful aspects of the Iterator pattern is how easily you can add new traversal behaviors without modifying the collection or client code.
Suppose tomorrow the product team wants two new features: play songs in reverse order, and play songs in a random shuffle. Without the Iterator pattern, you would add methods like playReverse() and playShuffle() to the player, each with its own loop logic. With the pattern, you just create new iterator classes.
Let us work through a second example to reinforce the pattern in a different domain. We are building a notification system where a NotificationCenter stores notifications of different types: email, SMS, and push. We need iterators that can traverse all notifications, filter by type, and show only unread notifications.
This is where the pattern really shines: three different ways to traverse the same collection, all behind the same interface. The client code is identical for each traversal mode, only the iterator creation changes.
The important thing to notice: the client loop is identical for all three iterators. while (hasNext()) { next() }. The filtering, skipping, and type-checking logic lives entirely inside the iterator classes. Adding a new traversal mode (say, "only push notifications from the last hour") means creating one new iterator class. The NotificationCenter and existing iterators remain unchanged.