Deep Dive
low level designdesign patternsoop

Iterator Pattern: Walk a Collection Without Knowing Its Shape

Every for-each loop you've ever written is the Iterator pattern in disguise. Here's how one hasNext()/next() surface lets you traverse an array, a tree, or an unbounded paged API the exact same way — and why mutating mid-loop blows up.

·16 min read
Easy

You wrote a for (item : collection) loop this morning and didn’t think about it once. That reflex is the payoff of a design decision made for you decades ago.

Picture the alternative. Your service holds recent user events in a ring buffer — a fixed-size array that overwrites its oldest entry when it fills, so the newest event might physically sit at index 2 while the oldest sits at index 5. Now a teammate needs to “just loop over the events.” To do it, they have to know there’s a head pointer, that logical order isn’t array order, and that reading the buffer means modular arithmetic against its capacity. Swap the ring buffer for a linked list next quarter and every one of those loops breaks — because each one reached into the structure’s guts.

The problem isn’t the ring buffer. It’s that how the collection stores its elements and how you walk them got welded together, so the caller can’t stay ignorant of the layout. The Gang of Four named the fix — and it’s the most-used pattern in the world.

The intuition: the channel-up button

Point a remote at your TV and press channel-up. You don’t know — or care — whether the tuner is scanning frequencies, stepping through a cable lineup, or hopping an IPTV playlist. The button gives you one verb: advance to the next thing. The tuner’s internals could be swapped entirely and your thumb would never notice.

That button is an iterator. The TV is the aggregate — the collection that knows how it’s really wired. The remote gives you a uniform way to move through the channels without ever exposing the wiring. hasNext() is “is there a channel after this one,” next() is the press.

The structure

Two roles, one factory method between them. The Aggregate exposes a createIterator() (in Java, iterator()); the Iterator it hands back carries the cursor and answers hasNext()/next(). The client talks only to the iterator interface.

iterator()hasNext()/next()implementsimplementscreatesClientfor-each loop«interface»Iterable · iterator()«interface»Iterator · hasNext()/next()RingBufferconcrete aggregateRingCursorconcrete iterator · owns head+seen
The client walks elements through the Iterator interface and never touches the concrete collection. iterator() is a factory: each call mints a fresh cursor, so independent traversals coexist.

The load-bearing arrow is RingBuffer → creates → RingCursor. The collection is the only thing that knows its own layout, so it’s the only thing qualified to build a cursor that walks it correctly. The client, on the far left, is wired to the two interfaces and never learns the word “RingBuffer.”

From here the members-only walkthrough builds the real thing: the two interfaces and a live ring-buffer implementation whose wrap-around hides entirely inside the cursor, the for-each-is-just-sugar desugaring, the external-vs-internal and eager-vs-lazy split (with a generator that streams a 42,000-request paginated API one page at a time), the fail-fast ConcurrentModificationException trap and its fix, a BST-iterator interview challenge, and a quiz to check yourself.

Members only

Keep reading with Premium

You've reached the members-only part of this deep-dive — the full implementation, the interactive ring simulator, and the step-by-step walkthrough. Unlock it with a membership.

Related Articles