SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignIterator Pattern
Software DesignBehavioral Patterns

Iterator Pattern

Sequentially access elements of a collection without exposing its underlying structure.

December 28, 20245 min read
design-patternsjavaiteratorbehavioral

One-line Definition

Abstracts the traversal of a collection so clients can iterate over it without knowing its internal data structure.


Problem

Collections store data in vastly different ways: arrays, linked lists, trees, hash maps, and graphs. If client code wants to iterate over a tree, it needs to know about tree traversal algorithms. If it wants to iterate over an array, it uses a simple for loop. This tightly couples the client to the collection's data structure. The Iterator Pattern extracts traversal logic into a separate object, giving clients a uniform way to loop over absolutely any collection.


Real-world Analogy

Think of a Spotify playlist. You press "Next" to move to the next song without knowing whether the playlist is stored locally as an array, streamed as a linked list, or dynamically fetched via a database query. The "Next" button is the iterator — it gives you one item at a time through a uniform interface.


Structure

  • Client
    • Uses the collection and iterator interfaces to traverse elements.
  • Iterator
    • Interface with traversal methods (e.g., hasNext(), next()).
  • ConcreteIterator
    • Implements the Iterator for a specific collection, tracking the current traversal position.
  • Aggregate (Iterable/Collection)
    • Interface declaring a method that returns an Iterator.
  • ConcreteAggregate
    • The actual data structure that creates and returns its compatible iterator.

Diagram

classDiagram
    class Client {
    }
    class Aggregate {
        <<interface>>
        +createIterator(): Iterator
    }
    class ConcreteAggregate {
        +createIterator(): Iterator
    }
    class Iterator {
        <<interface>>
        +hasNext(): boolean
        +next(): Object
    }
    class ConcreteIterator {
        -position
        +hasNext(): boolean
        +next(): Object
    }
    
    Client --> Aggregate
    Client --> Iterator
    ConcreteAggregate ..|> Aggregate
    ConcreteIterator ..|> Iterator
    ConcreteAggregate ..> ConcreteIterator : instantiates

Code Walkthrough

Notice that Main (the client) never accesses the internal array of the NotificationCollection. It exclusively relies on hasNext() and next().

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

interface Collection<T> {
    Iterator<T> createIterator();
}

class Notification {
    private final String message;
    public Notification(String message) { this.message = message; }
    public String getMessage() { return message; }
}

class NotificationCollection implements Collection<Notification> {
    private final Notification[] notifications;
    private int count = 0;

    public NotificationCollection(int capacity) {
        notifications = new Notification[capacity];
    }

    public void addNotification(Notification notification) {
        if (count < notifications.length) {
            notifications[count++] = notification;
        }
    }

    @Override
    public Iterator<Notification> createIterator() {
        return new NotificationIterator(notifications, count);
    }
}

class NotificationIterator implements Iterator<Notification> {
    private final Notification[] notifications;
    private final int count;
    private int position = 0;

    public NotificationIterator(Notification[] notifications, int count) {
        this.notifications = notifications;
        this.count = count;
    }

    @Override
    public boolean hasNext() { return position < count; }

    @Override
    public Notification next() { return notifications[position++]; }
}

class Main {
    public static void main(String[] args) {
        NotificationCollection collection = new NotificationCollection(10);
        collection.addNotification(new Notification("New message"));
        collection.addNotification(new Notification("Order shipped"));

        Iterator<Notification> iterator = collection.createIterator();
        while (iterator.hasNext()) {
            System.out.println("📩 " + iterator.next().getMessage());
        }
    }
}

Bad vs Good

Bad Approach

Problems

  • Client must know the collection uses an array and access it by an integer index.
  • Changing the internal structure (e.g., from an array to a List or a Tree) instantly breaks all client code.
class Main {
    public static void main(String[] args) {
        Notification[] notifications = getNotifications();
        
        // Client is tightly coupled to the array implementation
        for (int i = 0; i < notifications.length; i++) {
            if (notifications[i] != null) {
                System.out.println(notifications[i].getMessage());
            }
        }
    }
}

Better Approach

Improvements

  • Client uses hasNext()/next() without knowing the internal structure.
  • Collection can safely swap its internal data structure in the future without affecting a single line of client code.
Iterator<Notification> it = collection.createIterator();
while (it.hasNext()) {
    System.out.println(it.next().getMessage());
}
// Works identically whether collection is an array, tree, or linked list

Pros vs Cons

ProsCons
Provides a uniform traversal interface for all collectionsHeavy overkill for simple apps using standard Java collections
Decouples the client from complex data structuresRequires creating a new iterator class for every collection type
Supports multiple simultaneous, independent traversalsThe iterator can easily become stale if the collection is modified
Enforces the Single Responsibility Principle
Enables different traversal strategies (e.g., DFS vs BFS in trees)

When to Use

  • You want to provide a standard way to iterate over a custom, complex data structure (like a custom Graph or Tree).
  • You want to hide the internal representation of a collection from clients.
  • You need to support multiple traversal algorithms (e.g., Depth-First vs Breadth-First) across the same data.

When Not to Use

  • You are using standard Java collections (Lists, Sets, Maps). Standard Java Iterator handles this perfectly. Re-inventing it is redundant.
  • The collection is incredibly simple and will never change its internal structure.

Real-world Examples

  • java.util.Iterator and java.lang.Iterable (The backbone of the enhanced for-each loop).
  • java.util.Enumeration (Legacy equivalent).
  • java.sql.ResultSet (Acts as an iterator over database records).

Key Takeaway

The Iterator Pattern abstracts the complex logic of moving through a data structure, providing clients with a clean, universal way to loop over elements. It is so fundamental to modern programming that Java baked it directly into the language via Iterable. Use it when building custom data structures, but rely on Java's built-in iterators everywhere else.

PreviousStrategy PatternNextSingleton Pattern