skip to content

When is the Iterator pattern the wrong abstraction, and what alternatives would you reach for instead?

level: principalimportance: nice to knowfreq 24%

answer

  1. sequential = no seam for parallel splitting
  2. push the predicate down, don't drag the set to the loop
  3. heterogeneous types + many operations → Visitor
  4. event-driven producer → push + explicit demand
  5. per-element across a boundary = N round trips

basics

~20 s

Iterator assumes one element at a time, in order, pulled by the consumer. It is wrong when you need parallel processing, whole-set operations pushed down to a database, type-specific behaviour per element kind, or bulk transfer over a network where per-element calls are too chatty.

solid answer

~60 s

Iterator's contract — sequential, one-at-a-time, consumer-paced — is a *constraint* as much as an abstraction. Reach for something else when: - **Parallelism matters.** A strictly sequential cursor gives the runtime no way to split work; a splittable source (recursive decomposition, index ranges) plus internal iteration does. - **The set operation should be pushed down.** Filtering/aggregating a million rows by dragging them through a client-side loop is far worse than a `WHERE`/`GROUP BY`, an index lookup, or a bulk API call. Iterator makes an O(n) transfer look innocuous. - **Behaviour differs per element type** in a heterogeneous structure — Visitor (double dispatch) expresses that better than a uniform `next()` plus type tests. - **The producer must drive**, e.g. an event feed with no natural pull point — use a push/reactive protocol with explicit demand-based backpressure. - **Random access, indexing, or set algebra is the real requirement** — expose an indexable or query interface, not a cursor. Also avoid publishing an iterator where a small materialized collection is clearer: laziness adds lifetime, single-pass and failure semantics for no gain.

go deeper

for a junior

Say Iterator gives one element at a time and is a poor fit when you need the whole set at once or random access.

for a middle

Name concrete alternatives: bulk/batch APIs to avoid N+1, database-side filtering and aggregation, and a materialized list for small results.

for a senior

Discuss why sequential cursors block parallelism, the Visitor trade-off for heterogeneous structures, and push versus pull with backpressure.

for a principal

Frame it as choosing what contract to publish: pushdown versus client-side traversal at data-volume scale, splittable sources for parallel execution, the expression-problem trade-off behind Iterator/Visitor, and the operational cost of lazy cross-boundary cursors (connection pinning, resumability, chattiness).

## Iterator encodes four commitments When you publish an iterator you commit consumers to: **(1) one element at a time, (2) in a fixed order, (3) pulled by the consumer, (4) processed on the consumer's side**. Every situation below is one where at least one of those commitments is wrong. ### 1. When work should be parallel A `hasNext`/`next` cursor is definitionally a sequence of dependent steps: you cannot know element k+1's location without having done step k. That leaves a runtime no seam for splitting. Alternatives: - **Splittable sources + internal iteration.** Give the library a source it can *divide* (an index range, a tree's subtrees, a partitioned file) plus a callback, and it can fan out and recombine. Java's `Spliterator.trySplit`, fork/join decomposition, and map-reduce partitioning are all the same idea: replace "give me the next one" with "here is a chunk, and here is how to halve it". - Practical caveat: parallel traversal is a win only when per-element work is substantial and side-effect-free; splitting a linked list (which cannot be split cheaply) or parallelizing trivial work loses to overhead. ### 2. When the operation belongs to the data store The most expensive iterator bug in production is architectural: a loop that pulls a large result set into the application to filter, join, or aggregate it. The cursor abstraction makes `while (rows.hasNext())` look like a cheap for-loop while it moves gigabytes across a network. Prefer: - predicate/aggregate pushdown (`WHERE`, `GROUP BY`, index seek, projection of only the needed columns); - bulk/batch endpoints (`getUsers(ids)`) instead of a per-element fetch inside a loop — the N+1 problem in its most common form; - set-based operations (joins, upserts, `MERGE`) instead of row-by-row ("RBAR") processing. A useful rule: if the loop body's only job is to discard or fold elements, the loop probably belongs somewhere else. ### 3. When behaviour is type-dependent, not element-dependent Iterating a heterogeneous structure (an AST, a document model, a composite of shapes) and switching on the runtime type inside the loop is a smell. **Visitor** encodes the same traversal with double dispatch: each node type accepts a visitor and calls the matching method, so adding a new *operation* is one new class and the compiler tells you when a node type is unhandled. Iterator remains a fine *complement* — a Composite often provides an iterator for uniform work and accepts a visitor for type-specific work. Trade-off: Visitor makes adding new *node types* expensive (every visitor must change), the classic expression-problem tension; and Visitor's traversal order can be fixed by the structure rather than chosen by the client. ### 4. When the producer must drive the pace Some sources have no pull point: hardware interrupts, UI events, market data feeds, a broker delivering messages. Modelling them as pull requires the consumer to block a thread on `hasNext()`, which does not scale to thousands of sources. Alternatives: callbacks/observers, message-driven consumers, or reactive streams. But note the essential piece: **push must reintroduce backpressure explicitly** (a demand/credit signal such as `request(n)`), because you lose the natural throttling that pull gives you. Systems that push without demand either drop, buffer unboundedly (and OOM), or block the producer. ### 5. When the consumer needs more than sequence If callers need random access, binary search, sorting, size, containment tests, or slicing, an iterator is a downgrade — hand them an indexable collection or a query interface. Conversely, if callers only ever need "is there any element matching P", expose that predicate rather than a whole traversal, so the implementation can answer it with an index. ### 6. When laziness costs more than it saves Returning a lazy single-pass iterator for a 20-element in-memory result imports real complexity: lifetime/close semantics, non-repeatability, mid-traversal failure, and cursor invalidation if the source mutates. A small immutable list is simpler, safe to share, repeatable and thread-safe. Laziness earns its keep at scale, with infinite/unbounded sequences, or when early exit avoids substantial work. ### 7. Cross-boundary chattiness An iterator whose `next()` crosses a process, network, or FFI boundary per element multiplies latency by N. Either batch behind the cursor (page-at-a-time, invisible to the consumer) or switch the contract to bulk transfer/streaming of chunks. This is why remote iteration protocols always page. ## How to decide Ask what the *consumer* genuinely needs and what the *producer* can genuinely do cheaply: - consumer needs one-at-a-time, may stop early, paces itself → Iterator (external); - library needs to parallelize, fuse, or guarantee cleanup → internal iteration; - the set operation is expressible where the data lives → push it down, return the answer; - element kinds differ and operations multiply → Visitor; - the producer is event-driven → push with explicit demand; - the result is small and reused → materialized immutable collection. And remember these compose: a well-designed API commonly returns a *paged, closeable iterator over batches*, built on push-down filtering, with an internal-iteration convenience wrapper that guarantees the close.

  • Why can't a library parallelize a plain `hasNext`/`next` loop for you?
    Because each step depends on the previous one and the client's loop *is* the schedule — there is no place for the library to partition work. Parallelism needs a splittable source (index ranges, subtrees, file blocks) plus a callback the library can invoke on each chunk, which is why parallel APIs are built on internal iteration over splittable sources rather than on cursors.
  • Iterator and Visitor both traverse structures. How do you choose?
    Iterator when elements are treated uniformly and you mainly add new *data sources*; Visitor when elements are heterogeneous and you keep adding new *operations* over a stable set of node types, because double dispatch removes type switches and gives compile-time completeness checks. The trade-off is the expression problem: Visitor makes adding a new node type expensive.
  • You replace a pull-based cursor with a push-based stream to raise throughput. What must you add?
    Explicit backpressure — a demand/credit signal so the consumer states how much it can absorb, plus a policy (buffer bound, drop, or block) for when demand is exhausted. Without it, a fast producer either exhausts memory or the system silently loses data; pull gave you this for free.

An iterator is a conveyor belt handing you one item at a time. Great for inspecting parts, useless for weighing the whole shipment — for that you don't walk the belt, you ask the warehouse for the total. And if the belt crosses an ocean, you ship containers, not single items.

saying these in an interview costs you the question

  • Treating Iterator as always-correct and never asking whether the operation should run where the data lives (client-side filtering of a large result set).
  • Fetching one remote element per loop iteration — the N+1 pattern hidden behind a clean cursor.
  • Using Visitor for a homogeneous collection where a simple iteration would do, or Iterator plus a chain of runtime type checks where Visitor belongs.
  • Claiming parallel iteration is just "iterate on more threads" — without a splittable source you have no way to divide the traversal.
  • Returning lazy single-pass sequences everywhere, importing close/lifetime/repeatability problems for results that are small and reused.
  • Switching to push for throughput without a demand protocol, and discovering unbounded buffering under load.

context