The menu of La Bella Napoli is a Composite tree: sections inside sections, dishes at the leaves, combos that group things. PideYa's search engine, however, couldn't care less about the tree: it wants "all the dishes, one after another" so it can index them. The allergen screen wants the same; so does the price comparator. And today each one reimplements the recursion on its own, knowing about MenuSection and its children — the menu's internal structure, smeared across half the application. Iterator solves exactly this: traversing a collection without exposing how it is built inside. It is the GoF pattern you use most without knowing it — every for-each you write is Iterator in action — and this lesson uncovers it: from the classic structure to Java's Iterable, and from there to Streams as its natural evolution.

Contents

  1. The problem in PideYa: traversing the menu without knowing it
  2. Intent and structure of the pattern
  3. Iterator already lives in Java: Iterator, Iterable, and for-each
  4. Implementation: flattening the Composite menu
  5. Second case: paginated order history
  6. External vs. internal iterators, and Streams as evolution
  7. When to use it and when not to
  8. Relationship with other patterns
  9. Common mistakes
  10. Exercises and conclusion

The problem in PideYa: traversing the menu without knowing it

This is how the search engine indexes a menu's dishes today:

public class SearchIndexer {

    public void index(MenuComponent component) {
        if (component instanceof Dish dish) {
            index.add(dish);
        } else if (component instanceof MenuSection section) {
            for (MenuComponent child : section.getChildren()) {
                index(child);                    // hand-rolled recursion
            }
        } else if (component instanceof Combo combo) {
            // ...do a combo's dishes get indexed? another special case
        }
    }
}

And the allergen screen has its own copy of this recursion, and the price comparator has its own. The problems:

  • The internal structure has leaked to the clients: everyone knows there are sections with getChildren(), everyone does instanceof. If tomorrow MenuSection stores its children in a different order, or a new node type appears, you have to hunt down every copy of the recursion.
  • Duplicated traversal: the "visit the whole tree" logic is written N times, with N opportunities for N different bugs.
  • No partial traversals: "give me the first 10 dishes" forces you to traverse everything or dirty the recursion with counters and flags.
  • No simultaneous traversals: two traversals at once over the same menu (searching while exporting) require each to carry its own state — impossible if the traversal state lives in the menu itself.

What's missing is an object whose sole responsibility is the traversal: knowing where it is and how to advance. Reifying the traversal — the family's trick, once again.

Intent and structure of the pattern

Intent (GoF): provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

classDiagram
    class DishIterator {
        <<interface>>
        +hasNext() boolean
        +next() Dish
    }
    class Aggregate {
        <<interface>>
        +createIterator() DishIterator
    }
    class MenuSection {
        +createIterator() DishIterator
    }
    class MenuDepthIterator {
        -pending: Deque~MenuComponent~
        +hasNext() boolean
        +next() Dish
    }
    class Client["SearchIndexer (client)"]

    Aggregate <|.. MenuSection
    DishIterator <|.. MenuDepthIterator
    MenuSection ..> MenuDepthIterator : creates
    Client --> DishIterator : traverses with
    Client --> Aggregate : requests iterator from
GoF role In PideYa
Iterator (traversal interface) DishIterator (in real Java: Iterator<Dish>)
ConcreteIterator (knows where it is) MenuDepthIterator
Aggregate (interface that manufactures iterators) Aggregate (in real Java: Iterable<Dish>)
ConcreteAggregate MenuSection

Two capital ideas in the design:

  • The traversal state lives in the iterator, not in the collection. That's why several simultaneous, independent traversals can run over the same menu: each is a distinct iterator object.
  • createIterator() is a Factory Method: the concrete collection decides which concrete iterator it hands out, and the client sees only the interface. GoF points this out explicitly: the two patterns are born together.

Iterator already lives in Java: Iterator, Iterable, and for-each

Before implementing anything, recognize the pattern in the JDK, because it is copied straight from GoF:

public interface Iterator<E> {      // the Iterator role
    boolean hasNext();
    E next();
    default void remove() { throw new UnsupportedOperationException(); }
}

public interface Iterable<T> {      // the Aggregate role
    Iterator<T> iterator();         // the factory method
}

And for-each is syntactic sugar over the pattern. These two versions compile to the same thing:

for (Dish d : menu) {                        // what you write
    index.add(d);
}

Iterator<Dish> it = menu.iterator();         // what the JVM runs
while (it.hasNext()) {
    Dish d = it.next();
    index.add(d);
}

In other words: every time you write a for-each over a List, a Set, or a Map.entrySet(), you are using Iterator — with the internal representation (array, linked nodes, hash table, red-black tree) perfectly hidden behind hasNext()/next(). The practical takeaway for PideYa: don't invent DishIterator; implement Iterable<Dish> and Iterator<Dish>, and all your traversals get for-each for free.

Implementation: flattening the Composite menu

Goal: make MenuSection an Iterable<Dish> with a depth-first traversal that flattens nested sections — the client sees a stream of dishes, the tree disappears.

The classic technique for traversing a tree without recursion (recursion doesn't fit hasNext/next, which return control after each element) is a stack of pending work:

public class MenuDepthIterator implements Iterator<Dish> {

    private final Deque<MenuComponent> pending = new ArrayDeque<>();

    public MenuDepthIterator(MenuSection root) {
        pending.push(root);
        advanceToDish();
    }

    /** Pops until the top is a Dish (or nothing remains). */
    private void advanceToDish() {
        while (!pending.isEmpty() && !(pending.peek() instanceof Dish)) {
            MenuComponent current = pending.pop();
            if (current instanceof MenuSection section) {
                List<MenuComponent> children = section.getChildren();
                for (int i = children.size() - 1; i >= 0; i--) {
                    pending.push(children.get(i));   // reversed: the 1st ends up on top
                }
            }
            // Combo or other nodes without dishes of their own: discarded here
        }
    }

    @Override
    public boolean hasNext() {
        return !pending.isEmpty();
    }

    @Override
    public Dish next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        Dish dish = (Dish) pending.pop();
        advanceToDish();                              // stage the next one
        return dish;
    }
}

Points worth explaining:

  • The stack replaces the call stack of the recursion: instead of "a function calling itself", we store the nodes still to visit. Pushing the children in reverse order makes them come out in the menu's natural order.
  • The iterator's invariant: between calls, the top of the stack is always the next dish (or the stack is empty). advanceToDish() restores that invariant after each extraction; that makes hasNext() trivial and cheap.
  • NoSuchElementException in next() with no elements: it is Iterator's standard contract and clients (and for-each) count on it.

Now the collection hands out its iterator — and the instanceof disappears from every client:

public class MenuSection extends MenuComponent implements Iterable<Dish> {
    // ... everything it already had from module 3 ...

    @Override
    public Iterator<Dish> iterator() {
        return new MenuDepthIterator(this);
    }
}
public class SearchIndexer {
    public void index(MenuSection menu) {
        for (Dish dish : menu) {             // no instanceof, no getChildren(), no recursion
            index.add(dish);
        }
    }
}

Tomorrow the menu changes its internal structure, or we want to offer a breadth-first traversal or "available only"? You change (or add) an iterator; the clients stay untouched.

Second case: paginated order history

"My orders" shows the customer's full history, but the server serves it in pages of 20. The client code shouldn't know anything about pages — it just wants "the next order". The iterator is the perfect place to hide the pagination:

public class OrderHistoryIterator implements Iterator<Order> {

    private final HistoryService service;
    private final String customerId;
    private List<Order> currentPage = List.of();
    private int positionInPage = 0;
    private int pageNumber = 0;
    private boolean lastPage = false;

    public OrderHistoryIterator(HistoryService service, String customerId) {
        this.service = service;
        this.customerId = customerId;
    }

    @Override
    public boolean hasNext() {
        if (positionInPage < currentPage.size()) {
            return true;
        }
        if (lastPage) {
            return false;
        }
        currentPage = service.page(customerId, pageNumber++, 20); // lazy loading
        positionInPage = 0;
        lastPage = currentPage.size() < 20;
        return !currentPage.isEmpty();
    }

    @Override
    public Order next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        return currentPage.get(positionInPage++);
    }
}

The elegance is in what the client doesn't see: network requests, page sizes, last-page detection. And in what doesn't happen: if the UI only shows the first 20, page 2 is never requested — the iterator is lazy by construction, a cousin of the virtual proxy in spirit: don't do the work until it's asked for.

External vs. internal iterators, and Streams as evolution

Everything above is external iteration: the client controls the loop — it asks element by element with next(), and it can stop, interleave, or run two iterators at once. The alternative is internal iteration: you hand the collection the operation, and it controls the loop:

menu.forEach(dish -> index.add(dish));           // internal: the loop is inside
External (Iterator) Internal (forEach, Streams)
Who controls the loop The client The collection/stream
Stopping midway, interleaving two traversals Easy Hard or impossible
Conciseness, chaining operations Verbose Excellent
Parallelizing Manual parallelStream()

Java Streams (Java 8+) are internal iteration brought to maturity, and they descend directly from this pattern: a Stream is built on a Spliterator (an iterator with partitioning support for parallelism), it is lazy like our paginated iterator, and it hides the traversal behind declarative operations:

List<String> glutenFree = StreamSupport.stream(menu.spliterator(), false)
        .filter(Dish::isGlutenFree)
        .map(Dish::getName)
        .sorted()
        .toList();

Notice the dividend: because we made MenuSection implements Iterable, Streams come for free — menu.spliterator() ships as standard with Iterable. The modern practical rule: expose Iterable/Stream on your domain collections; write an Iterator by hand only when the traversal itself has logic worth encapsulating (flattening a tree, paginating an API, merging sources).

When to use it and when not to

Use it when:

  • Clients traverse a non-trivial structure (a tree, remote pages, multiple sources) and must not know its representation.
  • You want multiple traversals (natural order, filtered, breadth-first) or several simultaneous ones over the same collection.
  • You want lazy traversals over data that is expensive to fetch.

Avoid it when:

  • The collection is a plain List and the traversal runs start to finish: the JDK's for-each and Streams already are the pattern; writing your own Iterator would be reimplementing the standard.
  • The client needs positional or random access (get(i)): the iterator's sequential contract gets in the way.

The real cost: hand-written iterators have their own delicate logic (invariants, state between calls) and are a typical hotbed of off-by-one errors — write tests for them as for any stateful class.

Relationship with other patterns

  • Composite: a natural pair — the composite defines the tree, the iterator traverses it without exposing it. With Visitor they will form the full trio of "structure + traversal + operations".
  • Factory Method: iterator() is a textbook factory method.
  • Memento: GoF notes that an iterator can capture its position in a memento to resume it later.
  • Proxy: the paginated iterator shares spirit with the virtual proxy (laziness toward expensive resources).

Common mistakes

  • Modifying the collection while iterating: JDK iterators are fail-fast and throw ConcurrentModificationException. To delete during a traversal use iterator.remove(), removeIf(...), or iterate over a copy.
  • hasNext() with effects that repeat: clients may call hasNext() several times in a row; it must be idempotent (our paginated one loads a page only when the current one runs out, no matter how many times it's called).
  • Storing the traversal state in the collection (an "internal cursor"): it kills simultaneous traversals and leaves the collection in an inconsistent state if a traversal is abandoned midway.
  • Returning null as end-of-traversal instead of hasNext() == false + NoSuchElementException: it breaks the contract for-each and the whole ecosystem expect.
  • Iterators that expose what they came to hide: if your Iterator<MenuComponent> forces the client to instanceof what it receives, the problem is still there; choose the element type well (we return Dish, not MenuComponent).

Exercises

Exercise 1: filtering iterator

Implement AvailableDishIterator, which wraps any Iterator<Dish> and only hands out dishes with isAvailable() == true. (Hint: it's an iterator-decorator; you'll need to "look ahead" for the next valid one to be able to answer hasNext().)

Exercise 2: for-each over the history

Make the history usable with for-each: write OrderHistory implements Iterable<Order>, leaning on OrderHistoryIterator. Why is it important that iterator() return a new iterator on every call?

Exercise 3: hand-written iterator or Stream?

For each case, say whether you would write your own Iterator or use Streams/for-each over what exists: (a) summing the prices of a List<OrderLine>; (b) traversing today's orders while merging two sorted sources (app and phone) in global chronological order; (c) the 5 most expensive dishes on the menu.

Solutions

Solution 1:

public class AvailableDishIterator implements Iterator<Dish> {

    private final Iterator<Dish> source;
    private Dish upcoming;                 // the next valid one, already located

    public AvailableDishIterator(Iterator<Dish> source) {
        this.source = source;
        advance();
    }

    private void advance() {
        upcoming = null;
        while (source.hasNext()) {
            Dish candidate = source.next();
            if (candidate.isAvailable()) {
                upcoming = candidate;
                return;
            }
        }
    }

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

    @Override public Dish next() {
        if (upcoming == null) throw new NoSuchElementException();
        Dish result = upcoming;
        advance();
        return result;
    }
}

The "look ahead" is mandatory: without locating the next valid one, hasNext() couldn't answer truthfully. And yes, this is Decorator mechanics applied to iterators — same interface, wraps, filters.

Solution 2:

public class OrderHistory implements Iterable<Order> {

    private final HistoryService service;
    private final String customerId;

    public OrderHistory(HistoryService service, String customerId) {
        this.service = service;
        this.customerId = customerId;
    }

    @Override
    public Iterator<Order> iterator() {
        return new OrderHistoryIterator(service, customerId);   // new every time!
    }
}

It must be new on every call because the traversal state lives in the iterator: if iterator() always returned the same one, a second for-each would start where the first one ended (or two simultaneous traversals would trample each other's pages).

Solution 3: (a) a one-line Stream: lines.stream().map(OrderLine::getPrice).reduce(ZERO, BigDecimal::add) — nothing to encapsulate; (b) a hand-written iterator: the sorted merge of two sources is genuine traversal logic (compare heads, advance the smaller) that deserves its own class and its own tests; (c) a Stream over the Iterable we already have: stream(menu.spliterator(), false).sorted(comparing(Dish::getPrice).reversed()).limit(5).

Conclusion

Iterator reifies the traversal: the position and the advancing logic live in an object with two methods, the internal structure stays sealed, and multiple traversals — full, filtered, lazy, simultaneous — coexist without touching each other. In PideYa, the Composite menu flattens behind Iterable<Dish> (goodbye to the duplicated recursions with instanceof) and the history hides its network pagination behind a hasNext(). And you've seen the full genealogy: from GoF to the JDK's Iterator/Iterable, from for-each as syntactic sugar to Streams as internal, lazy, parallelizable iteration.

So far, patterns that organize the dialogue between two parties: client and collection, invoker and command. The next one attacks crowded conversations. In PideYa's delivery operation, ready orders, available couriers, and the kitchen call each other in a mesh where everyone knows everyone — and every new piece multiplies the connections. The solution: nobody talks to anybody... except through one. See you in Mediator.

© Copyright 2026. All rights reserved