Iterator Pattern

Introduction

The Iterator Pattern provides a way to access elements of a collection sequentially without exposing its underlying representation. Whether the data lives in an array, a linked list, a tree, or a paginated API, the iterator gives clients a uniform interface to traverse it: hasNext() and next().

This pattern separates the traversal logic from the collection itself, allowing multiple traversal strategies over the same data structure without modifying the collection class.

The Problem

You are building a service that fetches results from a paginated REST API. The API returns 20 items per page, and you need to process all items across all pages. The naive approach is to embed the pagination logic directly in your business code — tracking page numbers, checking for the last page, and concatenating results.

Now imagine another part of your codebase also needs to iterate over the same API but with different filtering. You end up duplicating pagination logic everywhere. Worse, if the API changes its pagination scheme (from page-based to cursor-based), you must update every call site.

The Solution

The Iterator Pattern encapsulates the pagination logic inside an iterator object. Clients simply call hasNext() and next() without knowing whether the data comes from one page or fifty. The iterator handles fetching the next page transparently.

This means your business code stays clean and focused on processing items, while the iterator handles the mechanics of traversal and data fetching.

Key Principle

Single Responsibility Principle. The collection is responsible for storing data. The iterator is responsible for traversing it. Neither takes on the other’s job, making both easier to maintain and extend.

Java Example

Scenario: An iterator that transparently traverses paginated API results.

import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

// Simulated API response
public class Page<T> {
    private final List<T> items;
    private final int currentPage;
    private final int totalPages;

    public Page(List<T> items, int currentPage, int totalPages) {
        this.items = items;
        this.currentPage = currentPage;
        this.totalPages = totalPages;
    }

    public List<T> getItems() { return items; }
    public int getCurrentPage() { return currentPage; }
    public int getTotalPages() { return totalPages; }
    public boolean hasNextPage() { return currentPage < totalPages; }
}

// Simulated API client
public class ProductApiClient {
    private final List<List<String>> pages = List.of(
        List.of("Laptop", "Keyboard", "Mouse"),
        List.of("Monitor", "Headphones", "Webcam"),
        List.of("USB Hub", "Desk Lamp")
    );

    public Page<String> fetchPage(int pageNumber) {
        System.out.println("  [API] Fetching page " + pageNumber + "...");
        int index = pageNumber - 1;
        return new Page<>(pages.get(index), pageNumber, pages.size());
    }
}

// Iterator for paginated results
public class PaginatedIterator<T> implements Iterator<T> {
    private final ProductApiClient apiClient;
    private Page<String> currentPage;
    private int itemIndex = 0;

    public PaginatedIterator(ProductApiClient apiClient) {
        this.apiClient = apiClient;
        this.currentPage = apiClient.fetchPage(1);
    }

    @Override
    public boolean hasNext() {
        if (itemIndex < currentPage.getItems().size()) {
            return true;
        }
        // Current page exhausted — check if more pages exist
        return currentPage.hasNextPage();
    }

    @Override
    @SuppressWarnings("unchecked")
    public T next() {
        // If current page is exhausted, fetch the next one
        if (itemIndex >= currentPage.getItems().size()) {
            if (!currentPage.hasNextPage()) {
                throw new NoSuchElementException("No more items");
            }
            currentPage = apiClient.fetchPage(
                currentPage.getCurrentPage() + 1);
            itemIndex = 0;
        }
        return (T) currentPage.getItems().get(itemIndex++);
    }
}

// Iterable collection that provides the iterator
public class ProductCatalog implements Iterable<String> {
    private final ProductApiClient apiClient;

    public ProductCatalog(ProductApiClient apiClient) {
        this.apiClient = apiClient;
    }

    @Override
    public Iterator<String> iterator() {
        return new PaginatedIterator<>(apiClient);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        ProductApiClient api = new ProductApiClient();
        ProductCatalog catalog = new ProductCatalog(api);

        System.out.println("Iterating over all products:\n");

        // Clean for-each loop — pagination is invisible
        int count = 1;
        for (String product : catalog) {
            System.out.println("  " + count++ + ". " + product);
        }

        System.out.println("\nProcessed all products across all pages.");
    }
}

Python Example

Same paginated API traversal in Python using the iterator protocol.

from dataclasses import dataclass


@dataclass
class Page:
    items: list[str]
    current_page: int
    total_pages: int

    @property
    def has_next_page(self) -> bool:
        return self.current_page < self.total_pages


# Simulated API client
class ProductApiClient:
    def __init__(self):
        self._pages = [
            ["Laptop", "Keyboard", "Mouse"],
            ["Monitor", "Headphones", "Webcam"],
            ["USB Hub", "Desk Lamp"],
        ]

    def fetch_page(self, page_number: int) -> Page:
        print(f"  [API] Fetching page {page_number}...")
        items = self._pages[page_number - 1]
        return Page(items, page_number, len(self._pages))


# Iterator for paginated results
class PaginatedIterator:
    def __init__(self, api_client: ProductApiClient):
        self._api_client = api_client
        self._current_page = api_client.fetch_page(1)
        self._item_index = 0

    def __iter__(self):
        return self

    def __next__(self) -> str:
        # If current page is exhausted, fetch the next one
        if self._item_index >= len(self._current_page.items):
            if not self._current_page.has_next_page:
                raise StopIteration
            next_page_num = self._current_page.current_page + 1
            self._current_page = self._api_client.fetch_page(next_page_num)
            self._item_index = 0

        item = self._current_page.items[self._item_index]
        self._item_index += 1
        return item


# Iterable collection that provides the iterator
class ProductCatalog:
    def __init__(self, api_client: ProductApiClient):
        self._api_client = api_client

    def __iter__(self) -> PaginatedIterator:
        return PaginatedIterator(self._api_client)


# Usage
if __name__ == "__main__":
    api = ProductApiClient()
    catalog = ProductCatalog(api)

    print("Iterating over all products:\n")

    # Clean for loop — pagination is invisible
    for count, product in enumerate(catalog, start=1):
        print(f"  {count}. {product}")

    print("\nProcessed all products across all pages.")

When to Use

  • Paginated data sources — When you need to traverse results from APIs, databases, or file systems that return data in chunks.
  • Hiding collection internals — When clients should not know whether the underlying data structure is a list, tree, hash map, or external service.
  • Multiple traversal strategies — When you need different ways to traverse the same collection (forward, reverse, filtered, depth-first, breadth-first).
  • Lazy evaluation — When loading all data upfront is expensive or impossible, and you want to fetch items on demand as the client iterates.

Real-World Usage

  • Java Iterator and Iterable — The foundation of Java’s for-each loop. Every collection in java.util implements Iterable.
  • Python Iterators and Generators — Python’s for loop uses the iterator protocol (__iter__ / __next__). Generators with yield are a concise way to create iterators.
  • JDBC ResultSet — Iterates over database query results row by row without loading the entire result set into memory.
  • Spring Data Page / Slice — Spring Data’s pagination abstractions use the iterator pattern to traverse large datasets from repositories.



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *