Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 37

    Iterator Pattern

    Iterator separates traversal from storage.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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):

    ts
    async function* fetchAllRows(baseUrl: string): AsyncGenerator<Row> {
    let pageToken: string | undefined
    do {
    const res = await fetch(`${baseUrl}?pageToken=${pageToken ?? ""}`)
    const page = await res.json()
    for (const row of page.items) yield row
    pageToken = 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:

    Lazy iterator
    Client for…of
    Uniform API
    next()
    Fetch one page
    Yield row
    Process
    done: true
    Stop
    Memory constant — one page in flight at a time.
    Paginated API iterator
    pageToken null
    First fetch
    GET /rows?page=N
    On demand
    Yield items
    From page
    Next token
    Until exhausted
    Pagination hidden inside iterator — business code sees stream.
    Materialize vs iterate
    < 10K rows?
    Array OK
    Millions?
    Iterator
    Infinite stream?
    Generator
    Constant memory
    Lazy wins
    Profile memory before building custom iterator.

    Informative example

    Before / after — materialize all pages to lazy iterator:

    ts
    // ❌ Materialize 40M rows — OOM at 12M
    async function getAllRows(): Promise<Row[]> {
    const all: Row[] = []
    let token: string | undefined
    do {
    const page = await fetchPage(token)
    all.push(...page.items)
    token = page.nextPageToken
    } while (token)
    return all
    }
    // ✅ Async generator — process as stream
    async function* rowStream(): AsyncGenerator<Row> {
    let token: string | undefined
    do {
    const page = await fetchPage(token)
    for (const row of page.items) yield row
    token = page.nextPageToken
    } while (token)
    }
    for await (const row of rowStream()) await processRow(row)

    Execution workflow

    1Custom iterator workflow
    1 / 4

    Hide representation

    Client uses iterator only.

    Not array index or page token.

    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.

    4 questions
    1IntermediateQuestionWhy custom iterator for paginated API?+

    Answer

    Hide pagination; constant memory; business code uses for await without page token logic.

    Follow-up

    40M rows?
    2AdvancedQuestionIterator vs materialize array?+

    Answer

    Materialize when small and random access needed. Iterator when large, streaming, or memory constrained.

    Follow-up

    When prefetch?
    3IntermediateQuestionImplement Iterator in JS?+

    Answer

    Object with [Symbol.iterator]() returning { next() { return { value, done } } } } or generator function*.

    Follow-up

    Async generator?
    4AdvancedQuestionConcurrent modification during iterate?+

    Answer

    Iterator may throw or behave undefined — use copy, immutable snapshot, or concurrent collections.

    Follow-up

    Java ConcurrentModificationException?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.