Episode 9 — System Design / 9.5 — Behavioral Design Patterns
9.5.d Iterator Pattern
Overview
The Iterator Pattern provides a way to access the elements of a collection sequentially without exposing its underlying representation. It extracts the traversal behavior into a separate object called an iterator.
JavaScript has this pattern built into the language through the Iteration Protocol (Symbol.iterator), for...of loops, and generator functions.
+--------------------------------------------------------------+
| ITERATOR PATTERN |
| |
| Collection Iterator |
| +-----------------+ +------------------+ |
| | [a, b, c, d] |-------->| - current: 0 | |
| | | creates | | |
| | [Symbol.iterator]() | + next() | |
| +-----------------+ | { value, done }| |
| | | |
| | + return() | |
| | (cleanup) | |
| +------------------+ |
| |
| for (const item of collection) { ... } |
| // Uses the iterator protocol under the hood |
+--------------------------------------------------------------+
JavaScript Iteration Protocol
+------------------------------------------------------------------+
| ITERABLE PROTOCOL |
| |
| An object is "iterable" if it has a [Symbol.iterator]() method |
| that returns an ITERATOR. |
| |
| ITERATOR PROTOCOL |
| |
| An "iterator" is an object with a next() method that returns: |
| { value: <any>, done: <boolean> } |
| |
| Built-in iterables in JavaScript: |
| - Arrays |
| - Strings |
| - Maps |
| - Sets |
| - NodeLists (DOM) |
| - TypedArrays |
| - arguments object |
| - Generator objects |
+------------------------------------------------------------------+
// How for...of works under the hood:
const arr = [10, 20, 30];
// This:
for (const value of arr) {
console.log(value);
}
// Is equivalent to this:
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
console.log(result.value); // 10, 20, 30
result = iterator.next();
}
Implementation 1: Custom Iterable Collection
// ============================================
// CUSTOM RANGE: Iterable number range
// ============================================
class Range {
constructor(start, end, step = 1) {
if (step === 0) throw new Error('Step cannot be zero');
this.start = start;
this.end = end;
this.step = step;
}
// Makes this object iterable (usable with for...of)
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next() {
if ((step > 0 && current <= end) || (step < 0 && current >= end)) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
},
// Optional: called when iterator is abandoned early (break, return, throw)
return() {
console.log('[Range] Iterator closed early');
return { value: undefined, done: true };
}
};
}
// Utility: convert to array
toArray() {
return [...this];
}
// Utility: count without creating an array
count() {
let n = 0;
for (const _ of this) n++;
return n;
}
}
// ============================================
// USAGE
// ============================================
const range = new Range(1, 10);
// for...of
for (const num of range) {
process.stdout.write(`${num} `);
}
// Output: 1 2 3 4 5 6 7 8 9 10
console.log();
// Spread operator
console.log([...new Range(0, 20, 5)]);
// [0, 5, 10, 15, 20]
// Destructuring
const [a, b, c] = new Range(100, 500, 100);
console.log(a, b, c); // 100 200 300
// Countdown
for (const n of new Range(5, 1, -1)) {
console.log(`${n}...`);
}
// 5... 4... 3... 2... 1...
// Works with Array.from
console.log(Array.from(new Range(1, 5)));
// [1, 2, 3, 4, 5]
Implementation 2: Linked List Iterator
// ============================================
// LINKED LIST with Iterator
// ============================================
class ListNode {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.size = 0;
}
append(value) {
const node = new ListNode(value);
if (!this.head) {
this.head = this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
this.size++;
return this;
}
prepend(value) {
const node = new ListNode(value);
node.next = this.head;
this.head = node;
if (!this.tail) this.tail = node;
this.size++;
return this;
}
// Forward iterator
[Symbol.iterator]() {
let current = this.head;
return {
next() {
if (current) {
const value = current.value;
current = current.next;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
// Reverse iterator (using a stack)
reverse() {
const self = this;
return {
[Symbol.iterator]() {
// Collect all values first (LinkedList doesn't have back pointers)
const values = [];
let current = self.head;
while (current) {
values.push(current.value);
current = current.next;
}
let index = values.length - 1;
return {
next() {
if (index >= 0) {
return { value: values[index--], done: false };
}
return { value: undefined, done: true };
}
};
}
};
}
// Filtered iterator
filter(predicate) {
const self = this;
return {
[Symbol.iterator]() {
let current = self.head;
return {
next() {
while (current) {
const value = current.value;
current = current.next;
if (predicate(value)) {
return { value, done: false };
}
}
return { value: undefined, done: true };
}
};
}
};
}
toString() {
return [...this].join(' -> ');
}
}
// ============================================
// USAGE
// ============================================
const list = new LinkedList();
list.append(10).append(20).append(30).append(40).append(50);
// Standard iteration
console.log('Forward:', [...list]); // [10, 20, 30, 40, 50]
console.log('Reverse:', [...list.reverse()]); // [50, 40, 30, 20, 10]
console.log('Evens:', [...list.filter(n => n % 20 === 0)]); // [20, 40]
// for...of
for (const value of list) {
console.log(value);
}
// Destructuring
const [first, second, ...rest] = list;
console.log(first, second, rest); // 10 20 [30, 40, 50]
// String representation
console.log(list.toString()); // "10 -> 20 -> 30 -> 40 -> 50"
Implementation 3: Generator Functions
Generator functions provide a simpler syntax for creating iterators using the function* and yield keywords.
// ============================================
// GENERATOR BASICS
// ============================================
// Simple generator
function* countdown(n) {
while (n > 0) {
yield n;
n--;
}
}
for (const num of countdown(5)) {
console.log(num); // 5, 4, 3, 2, 1
}
// Infinite generator (lazy!)
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take first 10 fibonacci numbers
function take(iterator, n) {
const result = [];
for (const value of iterator) {
result.push(value);
if (result.length >= n) break;
}
return result;
}
console.log(take(fibonacci(), 10));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// ============================================
// GENERATOR-BASED ITERATORS FOR COMPLEX STRUCTURES
// ============================================
// Tree traversal using generators
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
// Depth-first traversal
*[Symbol.iterator]() {
yield this.value;
for (const child of this.children) {
yield* child; // delegate to child's iterator
}
}
// Breadth-first traversal
*breadthFirst() {
const queue = [this];
while (queue.length > 0) {
const node = queue.shift();
yield node.value;
queue.push(...node.children);
}
}
// Depth-first with level info
*depthFirst(level = 0) {
yield { value: this.value, level };
for (const child of this.children) {
yield* child.depthFirst(level + 1);
}
}
}
// Build a tree
const tree = new TreeNode('root', [
new TreeNode('A', [
new TreeNode('A1'),
new TreeNode('A2'),
]),
new TreeNode('B', [
new TreeNode('B1', [
new TreeNode('B1a'),
new TreeNode('B1b'),
]),
]),
new TreeNode('C'),
]);
console.log('DFS:', [...tree]);
// ['root', 'A', 'A1', 'A2', 'B', 'B1', 'B1a', 'B1b', 'C']
console.log('BFS:', [...tree.breadthFirst()]);
// ['root', 'A', 'B', 'C', 'A1', 'A2', 'B1', 'B1a', 'B1b']
for (const { value, level } of tree.depthFirst()) {
console.log(`${' '.repeat(level)}${value}`);
}
// root
// A
// A1
// A2
// B
// B1
// B1a
// B1b
// C
Implementation 4: Lazy Evaluation Pipelines
// ============================================
// LAZY ITERABLE OPERATORS
// ============================================
class LazyIterable {
constructor(source) {
this.source = source;
}
static from(iterable) {
return new LazyIterable(iterable);
}
static range(start, end, step = 1) {
return new LazyIterable({
*[Symbol.iterator]() {
for (let i = start; step > 0 ? i <= end : i >= end; i += step) {
yield i;
}
}
});
}
static infinite(startOrFn = 0) {
if (typeof startOrFn === 'function') {
return new LazyIterable({
*[Symbol.iterator]() {
let i = 0;
while (true) yield startOrFn(i++);
}
});
}
return new LazyIterable({
*[Symbol.iterator]() {
let i = startOrFn;
while (true) yield i++;
}
});
}
// Lazy map: transforms each element
map(fn) {
const source = this.source;
return new LazyIterable({
*[Symbol.iterator]() {
let index = 0;
for (const item of source) {
yield fn(item, index++);
}
}
});
}
// Lazy filter: only yields matching elements
filter(predicate) {
const source = this.source;
return new LazyIterable({
*[Symbol.iterator]() {
let index = 0;
for (const item of source) {
if (predicate(item, index++)) {
yield item;
}
}
}
});
}
// Lazy take: limits the number of elements
take(n) {
const source = this.source;
return new LazyIterable({
*[Symbol.iterator]() {
let count = 0;
for (const item of source) {
if (count >= n) return;
yield item;
count++;
}
}
});
}
// Lazy skip: skips first n elements
skip(n) {
const source = this.source;
return new LazyIterable({
*[Symbol.iterator]() {
let count = 0;
for (const item of source) {
if (count >= n) {
yield item;
}
count++;
}
}
});
}
// Lazy takeWhile: yields while predicate is true
takeWhile(predicate) {
const source = this.source;
return new LazyIterable({
*[Symbol.iterator]() {
for (const item of source) {
if (!predicate(item)) return;
yield item;
}
}
});
}
// Lazy flatMap
flatMap(fn) {
const source = this.source;
return new LazyIterable({
*[Symbol.iterator]() {
for (const item of source) {
yield* fn(item);
}
}
});
}
// Lazy zip: combines two iterables
zip(other) {
const source = this.source;
return new LazyIterable({
*[Symbol.iterator]() {
const iter1 = source[Symbol.iterator]();
const iter2 = other[Symbol.iterator]();
while (true) {
const a = iter1.next();
const b = iter2.next();
if (a.done || b.done) return;
yield [a.value, b.value];
}
}
});
}
// Terminal operations (these consume the iterator)
toArray() {
return [...this.source];
}
forEach(fn) {
let index = 0;
for (const item of this.source) {
fn(item, index++);
}
}
reduce(fn, initial) {
let acc = initial;
for (const item of this.source) {
acc = fn(acc, item);
}
return acc;
}
first() {
for (const item of this.source) {
return item;
}
return undefined;
}
count() {
let n = 0;
for (const _ of this.source) n++;
return n;
}
// Make LazyIterable itself iterable
[Symbol.iterator]() {
return this.source[Symbol.iterator]();
}
}
// ============================================
// USAGE: Lazy pipeline
// ============================================
// Process an infinite sequence lazily
const result = LazyIterable
.infinite(0) // 0, 1, 2, 3, 4, 5, ... (infinite!)
.filter(n => n % 2 === 0) // 0, 2, 4, 6, 8, ... (still infinite)
.map(n => n * n) // 0, 4, 16, 36, 64, ... (still infinite)
.skip(2) // 16, 36, 64, 100, ... (still infinite)
.take(5) // 16, 36, 64, 100, 144 (finite now)
.toArray(); // Actually evaluate
console.log(result); // [16, 36, 64, 100, 144]
// Only 5 values were ever computed! Not millions.
// Fibonacci numbers less than 100
const fibs = LazyIterable
.from({ *[Symbol.iterator]() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}})
.takeWhile(n => n < 100)
.toArray();
console.log(fibs); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
// Zip names with indices
const names = ['Alice', 'Bob', 'Charlie'];
const indexed = LazyIterable
.from(names)
.zip(LazyIterable.infinite(1))
.toArray();
console.log(indexed); // [['Alice', 1], ['Bob', 2], ['Charlie', 3]]
Implementation 5: Pagination Iterator
// ============================================
// ASYNC PAGINATION ITERATOR
// ============================================
class PaginatedAPI {
constructor(baseUrl, pageSize = 10) {
this.baseUrl = baseUrl;
this.pageSize = pageSize;
}
// Async iterable protocol
[Symbol.asyncIterator]() {
let page = 1;
let hasMore = true;
const self = this;
return {
async next() {
if (!hasMore) {
return { value: undefined, done: true };
}
try {
console.log(`[API] Fetching page ${page}...`);
const response = await self._fetchPage(page);
hasMore = response.hasNextPage;
page++;
return {
value: {
data: response.data,
page: response.page,
total: response.total
},
done: false
};
} catch (error) {
console.error(`[API] Error fetching page ${page}:`, error);
return { value: undefined, done: true };
}
},
return() {
hasMore = false;
console.log('[API] Pagination stopped');
return { value: undefined, done: true };
}
};
}
// Simulate API call
async _fetchPage(page) {
await new Promise(r => setTimeout(r, 50)); // Simulate latency
const totalItems = 47; // Simulated total
const start = (page - 1) * this.pageSize;
const end = Math.min(start + this.pageSize, totalItems);
if (start >= totalItems) {
return { data: [], page, total: totalItems, hasNextPage: false };
}
const data = Array.from(
{ length: end - start },
(_, i) => ({
id: start + i + 1,
name: `Item ${start + i + 1}`,
score: Math.random().toFixed(2)
})
);
return {
data,
page,
total: totalItems,
hasNextPage: end < totalItems
};
}
// Helper: iterate over individual items (not pages)
items() {
const paginatedIterator = this[Symbol.asyncIterator]();
return {
[Symbol.asyncIterator]() {
let currentBatch = [];
let batchIndex = 0;
let paginationDone = false;
return {
async next() {
// If we have items in current batch, return next one
if (batchIndex < currentBatch.length) {
return { value: currentBatch[batchIndex++], done: false };
}
// Fetch next page
if (!paginationDone) {
const pageResult = await paginatedIterator.next();
if (pageResult.done || pageResult.value.data.length === 0) {
paginationDone = true;
return { value: undefined, done: true };
}
currentBatch = pageResult.value.data;
batchIndex = 1; // Return first item, advance index
return { value: currentBatch[0], done: false };
}
return { value: undefined, done: true };
}
};
}
};
}
}
// ============================================
// USAGE
// ============================================
async function demo() {
const api = new PaginatedAPI('https://api.example.com/items', 10);
// Iterate over pages
console.log('=== Iterating Pages ===');
for await (const page of api) {
console.log(`Page ${page.page}: ${page.data.length} items (total: ${page.total})`);
// Break early if we want
if (page.page >= 2) {
console.log('Stopping after page 2');
break;
}
}
// Iterate over individual items
console.log('\n=== Iterating Items ===');
let count = 0;
for await (const item of api.items()) {
console.log(` ${item.id}: ${item.name} (score: ${item.score})`);
count++;
if (count >= 5) break; // Just show first 5 items
}
}
// demo();
Generator-Based Async Patterns
// ============================================
// ASYNC GENERATOR: Stream processing
// ============================================
async function* streamLines(filePath) {
// Simulating reading a file line by line
const lines = [
'id,name,age',
'1,Alice,30',
'2,Bob,25',
'3,Charlie,35',
'4,Diana,28'
];
for (const line of lines) {
await new Promise(r => setTimeout(r, 10)); // Simulate I/O
yield line;
}
}
async function* parseCSV(lineIterator) {
let headers = null;
for await (const line of lineIterator) {
const values = line.split(',');
if (!headers) {
headers = values;
continue; // Skip header row
}
const record = {};
headers.forEach((h, i) => record[h] = values[i]);
yield record;
}
}
async function* filterAge(recordIterator, minAge) {
for await (const record of recordIterator) {
if (parseInt(record.age) >= minAge) {
yield record;
}
}
}
// Pipeline: stream -> parse -> filter
async function processCSV() {
const lines = streamLines('data.csv');
const records = parseCSV(lines);
const filtered = filterAge(records, 28);
for await (const record of filtered) {
console.log(`${record.name} (age ${record.age})`);
}
// Alice (age 30)
// Charlie (age 35)
// Diana (age 28)
}
// processCSV();
Iterator vs Array Methods
+------------------------------------------------------------------+
| |
| ARRAY METHODS (Eager) ITERATORS (Lazy) |
| |
| const result = data const result = Lazy.from(data) |
| .filter(x => x > 10) .filter(x => x > 10) |
| .map(x => x * 2) .map(x => x * 2) |
| .slice(0, 5); .take(5) |
| .toArray(); |
| |
| - Creates intermediate arrays - No intermediate arrays |
| - Processes ALL items - Processes only needed items |
| - Data must fit in memory - Can handle infinite streams |
| - Simple, familiar API - More complex setup |
| - Good for small datasets - Good for large/infinite data |
| |
| When to use eager (Array): When to use lazy (Iterator): |
| - Small arrays (< 10K items) - Large datasets |
| - Need all results - Only need first N results |
| - Random access needed - Streaming data |
| - Simple transformations - Infinite sequences |
| - Memory-constrained |
+------------------------------------------------------------------+
Key Takeaways
- Iterators decouple traversal from data structures -- you can iterate over arrays, linked lists, trees, and APIs with the same
for...ofsyntax - JavaScript's iteration protocol (
Symbol.iterator+next()) is the standard way to make objects iterable - Generator functions (
function*+yield) provide a clean syntax for creating iterators - Lazy evaluation means values are computed only when requested -- crucial for infinite sequences and large datasets
- Async iterators (
Symbol.asyncIterator+for await...of) handle pagination, streams, and network data - Compose generators into pipelines -- each stage lazily transforms data from the previous stage
Explain-It Challenge
You're building a search engine that needs to merge results from 5 different data sources (database, cache, external API, file system, CDN). Each source returns results in ranked order. Design an iterator that lazily merges these sources and returns the top K results without fetching all results from all sources. How would you handle: (a) one source being slow, (b) duplicate results across sources, (c) a source returning an error mid-iteration?