Iterator Pattern
Iterator separates traversal from storage.
Introduction
Iterator separates traversal from storage. Clients walk collections through uniform interface (hasNext/next, for…of) without knowing if it's array, tree, paginated API, or DB cursor. Languages bake it in — for…of, Iterable, generators — but custom iterators remain essential for pagination, streaming, and lazy evaluation.
The story
A data team processed 40M rows from paginated REST API. First implementation pulled all pages into memory and crashed at 12M. Custom async iterator fetching next page on demand became constant-memory stream completing in 90 minutes.
The business problem
Materializing large or infinite sequences blows memory and couples to storage:
- Memory blowup: fetch all pages before processing starts.
- Coupling: callers reach into collection internals to traverse.
- Duplicated traversal: wrong iteration logic copied per consumer.
- Pagination leak: page tokens scattered in business code.
The problem teams faced
Iterator fits when:
- Traversal logic should hide underlying representation.
- Sequence is large, infinite, or lazily fetched.
- Multiple traversal strategies over same aggregate (BFS vs DFS).
- Pagination/streaming should not leak into business logic.
Understanding the topic
Intent: Access aggregate elements sequentially without exposing representation.
- Iterator — hasNext/next or Symbol.iterator.
- ConcreteIterator — holds traversal state (page cursor, tree stack).
- Aggregate — returns Iterator.
- Generator functions — idiomatic lazy Iterator in JS/TS.
Internal architecture
Async paginated iterator (TS):
async function* fetchAllRows(baseUrl: string): AsyncGenerator<Row> {let pageToken: string | undefineddo {const res = await fetch(`${baseUrl}?pageToken=${pageToken ?? ""}`)const page = await res.json()for (const row of page.items) yield rowpageToken = page.nextPageToken} while (pageToken)}for await (const row of fetchAllRows("/api/rows")) {await processRow(row) // constant memory}
Visual explanation
Three diagrams cover lazy iteration, pagination, and vs materialize:
Informative example
Before / after — materialize all pages to lazy iterator:
// ❌ Materialize 40M rows — OOM at 12Masync function getAllRows(): Promise<Row[]> {const all: Row[] = []let token: string | undefineddo {const page = await fetchPage(token)all.push(...page.items)token = page.nextPageToken} while (token)return all}// ✅ Async generator — process as streamasync function* rowStream(): AsyncGenerator<Row> {let token: string | undefineddo {const page = await fetchPage(token)for (const row of page.items) yield rowtoken = page.nextPageToken} while (token)}for await (const row of rowStream()) await processRow(row)
Execution workflow
Hide representation
Client uses iterator only.
Real-world use
Java Iterable/Iterator, JS for…of and generators, Python __iter__, DB cursors, Kafka consumers, Rust iterators, lazy streams in Java 8+.
Production case study
40M row paginated REST job:
- Scenario: Paginated API; materialized all pages — OOM at 12M rows.
- Decision: Custom async iterator; fetch page on demand.
- Outcome: Constant memory; job completed 90 minutes processing 40M rows.
Trade-offs
- Pro: uniform traversal; lazy memory; hides pagination.
- Pro: multiple iterators over same aggregate independently.
- Con: can't random-access without separate API.
- Con: iterator invalidation if aggregate mutates during traverse.
Decision framework
- Use custom iterator for large/paginated/infinite sequences.
- Use built-in for…of when aggregate is small in-memory array.
- Avoid materializing millions of rows "for simplicity."
Best practices
- async function* for paginated HTTP in Node/TS.
- Don't expose page tokens outside iterator implementation.
- Handle iterator cleanup (close DB cursor) in finally/return().
Anti-patterns to avoid
- fetchAll().then(process) for unbounded datasets.
- Exposing internal array for "performance" — breaks encapsulation.
Common mistakes
- Modifying collection during iteration — ConcurrentModificationException class of bugs.
- Forgetting to await in async iterator consumer loop.
Debugging tips
- Log page fetch count and rows yielded — verify lazy behaviour.
- Memory profile: flat heap during stream vs spike on materialize.
Optimization strategies
- Prefetch next page while processing current — pipeline fetch.
- Batch process inside loop (process 100 rows per tick).
Common misconceptions
- Iterator ≠ Observer. Iterator pulls sequence; Observer pushes events.
- for…of is Iterator — syntax sugar over protocol.
- Generators are Iterator in JS — idiomatic lazy form.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionWhy custom iterator for paginated API?+
Answer
Follow-up
2AdvancedQuestionIterator vs materialize array?+
Answer
Follow-up
3IntermediateQuestionImplement Iterator in JS?+
Answer
Follow-up
4AdvancedQuestionConcurrent modification during iterate?+
Answer
Follow-up
Summary
You can build paginated async iterators and avoid OOM on large datasets. Tell the 40M row / 12M crash story — if you can do that, you own Iterator.