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
- The problem in PideYa: traversing the menu without knowing it
- Intent and structure of the pattern
- Iterator already lives in Java:
Iterator,Iterable, and for-each - Implementation: flattening the Composite menu
- Second case: paginated order history
- External vs. internal iterators, and Streams as evolution
- When to use it and when not to
- Relationship with other patterns
- Common mistakes
- 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 doesinstanceof. If tomorrowMenuSectionstores 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 makeshasNext()trivial and cheap. NoSuchElementExceptioninnext()with no elements: it isIterator'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:
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
Listand the traversal runs start to finish: the JDK's for-each and Streams already are the pattern; writing your ownIteratorwould 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 useiterator.remove(),removeIf(...), or iterate over a copy. hasNext()with effects that repeat: clients may callhasNext()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
nullas end-of-traversal instead ofhasNext() == 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 toinstanceofwhat it receives, the problem is still there; choose the element type well (we returnDish, notMenuComponent).
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.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
