Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Iterator for Paginating

Problem Statement

Build a lazy-loading iterator that transparently fetches paginated data from REST APIs. Your iterator should handle pagination logic internally, making API endpoints with pagination appear as infinite streams of items.

Your paginated iterator should support:

  • Automatic page fetching as iteration progresses
  • Configurable page size and rate limiting
  • Error handling and retry logic
  • Caching to avoid redundant requests
  • Multiple pagination styles (offset-based, cursor-based, page-number-based)
  • Type-safe deserialization of API responses

Example API response:

{
  "data": [
    {"id": 1, "name": "Item 1"},
    {"id": 2, "name": "Item 2"}
  ],
  "pagination": {
    "next_cursor": "abc123",
    "has_more": true
  }
}

Why It Matters

Many REST APIs return data in pages to limit response sizes. Writing pagination logic manually is tedious and error-prone - you must track page numbers, handle edge cases, and manage state across requests. A paginated iterator abstracts this complexity, letting developers write .filter().map().collect() instead of nested loops with error handling.

This pattern is fundamental to API client libraries. Real-world examples: GitHub API, Stripe API, AWS SDK pagination, Google Cloud APIs - all use this pattern.


Key Concepts Explained

This project demonstrates how to build production-ready API clients using Rust’s iterator trait and advanced patterns.

1. Iterator Trait for Lazy Evaluation

Iterators process items on-demand, not all at once:

#![allow(unused)]
fn main() {
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// Lazy: Fetches pages only when items consumed
let iter = PaginatedIterator::new(url, 10);
for item in iter.take(5) {  // Only fetches first page!
    process(item);
}
}

vs Eager loading:

#![allow(unused)]
fn main() {
// ❌ Loads all pages into memory
let all_items = fetch_all_pages();  // OOM for large datasets
all_items.into_iter().take(5);

// ✅ Loads pages on-demand
PaginatedIterator::new().take(5);  // Constant memory
}

2. Generic Types with Trait Bounds

Type-safe deserialization for any data type:

#![allow(unused)]
fn main() {
struct PaginatedIterator<T>
where
    T: for<'de> Deserialize<'de>  // T must be deserializable
{
    buffer: Vec<T>,
}

// Works with any deserializable type
let users: PaginatedIterator<User> = PaginatedIterator::new();
let posts: PaginatedIterator<Post> = PaginatedIterator::new();
}

Benefit: One implementation works for all API response types.

3. PhantomData for Zero-Cost Type Parameters

Associate type parameter without storing values:

#![allow(unused)]
fn main() {
struct PaginatedIterator<T> {
    url: String,
    buffer: Vec<T>,
    _phantom: PhantomData<T>,  // 0 bytes!
}
}

4. Buffering for Performance

Fetch pages in batches, yield items individually:

#![allow(unused)]
fn main() {
struct PaginatedIterator<T> {
    buffer: Vec<T>,       // Current page items
    buffer_index: usize,  // Position in buffer
}

fn next(&mut self) -> Option<T> {
    if self.buffer_index >= self.buffer.len() {
        self.fetch_next_page()?;  // Fetch when exhausted
        self.buffer_index = 0;
    }
    let item = self.buffer[self.buffer_index].clone();
    self.buffer_index += 1;
    Some(item)
}
}

5. Enum for Multiple Strategies

Pattern match on pagination type:

#![allow(unused)]
fn main() {
enum PaginationStrategy {
    Offset { offset: usize, limit: usize },
    Cursor { cursor: Option<String>, page_size: usize },
    PageNumber { page: usize, per_page: usize },
}

fn fetch_page(&mut self) -> Result<()> {
    match &self.strategy {
        PaginationStrategy::Offset { offset, limit } =>
            fetch_with_offset(*offset, *limit),
        PaginationStrategy::Cursor { cursor, .. } =>
            fetch_with_cursor(cursor.as_ref()),
        PaginationStrategy::PageNumber { page, per_page } =>
            fetch_with_page_number(*page, *per_page),
    }
}
}

6. Token Bucket Algorithm for Rate Limiting

Control request rate to respect API limits:

#![allow(unused)]
fn main() {
struct RateLimiter {
    tokens: f64,           // Available tokens
    max_tokens: f64,       // Bucket capacity
    refill_rate: f64,      // Tokens/second
    last_refill: Instant,  // Last refill time
}

fn acquire(&mut self) {
    self.refill();  // Add tokens based on elapsed time
    while self.tokens < 1.0 {
        sleep(calculate_wait_time());
        self.refill();
    }
    self.tokens -= 1.0;
}
}

7. Exponential Backoff for Retries

Handle transient failures with increasing delays:

#![allow(unused)]
fn main() {
struct RetryPolicy {
    max_attempts: usize,
    initial_backoff: Duration,
    multiplier: f64,  // Usually 2.0
}

fn delay_for_attempt(&self, attempt: usize) -> Duration {
    let delay = self.initial_backoff * self.multiplier.powi(attempt as i32);
    delay.min(self.max_backoff)  // Cap maximum delay
}

// Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, ...
}

8. Arc and Mutex for Shared State

Share cache between iterator clones:

#![allow(unused)]
fn main() {
struct CachedIterator<T> {
    cache: Arc<Mutex<Vec<Vec<T>>>>,  // Shared ownership
}

fn clone_iter(&self) -> Self {
    CachedIterator {
        cache: Arc::clone(&self.cache),  // Reference count++
    }
}
}

9. Builder Pattern for Configuration

Fluent API for complex setup:

#![allow(unused)]
fn main() {
let iter = PaginatedIterator::new(url, 10)
    .with_rate_limit(5.0)               // 5 req/sec
    .with_retries(3, Duration::from_secs(1))
    .with_timeout(Duration::from_secs(30))
    .with_cache();

// vs verbose constructor
let iter = PaginatedIterator::new_with_all_options(
    url, 10, Some(5.0), Some((3, Duration::from_secs(1))), ...
);
}

10. Higher-Order Iterator Adapters

Chain operations without intermediate allocations:

#![allow(unused)]
fn main() {
let filtered_users = PaginatedIterator::<User>::new(url, 100)
    .filter(|u| u.active)           // Lazy filter
    .map(|u| u.email)               // Lazy map
    .take(50)                        // Lazy take
    .collect::<Vec<_>>();           // Execute chain

// Only fetches pages needed for 50 active users
// If first page has 50 active users, only 1 HTTP request!
}

Connection to This Project

Here’s how each milestone applies these concepts to build production-grade API clients.

Milestone 1: Basic Paginated Iterator

Concepts applied:

  • Iterator trait: Implement next() for lazy evaluation
  • Generic types: PaginatedIterator<T> works with any type
  • Buffering: Fetch pages, yield items individually
  • PhantomData: Track type parameter without storing values

Why this matters: Foundation of lazy, type-safe pagination.

Real-world impact:

  • GitHub API: 5000 requests/hour limit
  • Without pagination: Load all repositories → OOM or quota exceeded
  • With lazy iterator: Load only visible items → constant memory

Performance (100K items, 100 items/page):

ApproachMemoryAPI CallsTime
Eager load all100K items100030s
Iterator (take 500)500 items50.5s

Milestone 2: Multiple Pagination Strategies

Concepts applied:

  • Enum dispatch: Pattern match on PaginationStrategy
  • Cursor stability: Handles concurrent data changes
  • Trait bounds: T: Deserialize for response parsing

Why this matters: Different APIs use different pagination styles.

Comparison:

StrategyProsConsUse Case
OffsetSimple, statelessSkips/duplicates with concurrent writesStatic data
CursorStable, no skipsOpaque tokens, can’t jumpReal-time feeds
Page numberHuman-friendly URLsSkips/duplicates with changesWeb UIs

Real-world example: Twitter API uses cursors because tweets are constantly added/deleted.

Stability test:

  • 1000 items in database
  • Fetch page 1 (items 0-99)
  • Insert 50 items at beginning
  • Offset page 2: Gets items 150-249 (skips items 100-149)
  • Cursor page 2: Gets items 100-199 (stable)

Milestone 3: Rate Limiting and Retry Logic

Concepts applied:

  • Token bucket algorithm: Smooth rate limiting
  • Exponential backoff: Handle transient failures
  • Builder pattern: .with_rate_limit(5.0)

Why this matters: Production APIs have strict rate limits.

Rate limit examples:

  • GitHub: 5000 requests/hour = 1.39 req/sec
  • Stripe: 100 requests/sec (burst)
  • Twitter: 300 requests/15min = 0.33 req/sec

Without rate limiting:

#![allow(unused)]
fn main() {
// Burst 100 requests immediately
for i in 0..100 {
    fetch_page(i);  // 429 error after ~10 requests
}
// Result: IP banned, data incomplete
}

With rate limiting:

#![allow(unused)]
fn main() {
let iter = PaginatedIterator::new(url, 10)
    .with_rate_limit(1.0);  // 1 req/sec

for item in iter.take(100) {
    process(item);  // Automatically throttled
}
// Result: Completes successfully in ~10 seconds
}

Retry logic impact (95% network success rate):

ScenarioNo Retries3 RetriesSuccess Rate
100 requests~95 succeed~99.9 succeed5× fewer failures
Transient error (server restart)Fails immediatelySucceeds after 200msResilient

Milestone 4: Caching for Re-Iteration

Concepts applied:

  • Arc/Mutex: Shared cache between clones
  • Reference counting: Automatic memory management
  • Clone-on-share: Multiple iterators, one cache

Why this matters: Exploratory data analysis requires multiple passes.

Use case: Data exploration

#![allow(unused)]
fn main() {
let iter = PaginatedIterator::new(url, 100).with_cache();

// First pass: Load all data (makes HTTP requests)
let active_count = iter.clone_iter()
    .filter(|u| u.active)
    .count();  // Fetches all pages, caches them

// Second pass: Uses cache (no HTTP requests!)
let admin_emails = iter.clone_iter()
    .filter(|u| u.role == "admin")
    .map(|u| u.email)
    .collect();  // Instant! Uses cached data

// Third pass: Also instant
let avg_age = iter.clone_iter()
    .map(|u| u.age)
    .sum::<u32>() / iter.clone_iter().count() as u32;
}

Performance comparison (10K items):

PassWithout CacheWith CacheSpeedup
1st5s (HTTP)5s (HTTP + cache)Same
2nd5s (HTTP again)0.01s (cache)500× faster
3rd5s (HTTP again)0.01s (cache)500× faster

Memory trade-off:

  • Cache memory: ~10KB per 100 items
  • 10K items = ~1MB cached
  • Worth it for multiple iterations

API quota savings:

  • 3 passes without cache: 300 API calls
  • 3 passes with cache: 100 API calls (66% reduction)

Project-Wide Benefits

Concrete comparisons - Processing 50K GitHub repositories:

MetricManual PaginationBasic IteratorOptimized IteratorImprovement
Code lines~150~30~530× less code
Memory usage50K repos (~50MB)Current page (~50KB)Cached (~1MB)50× less
API calls (3 passes)1500150050066% reduction
Rate limit errorsFrequentFrequentNoneEliminated
Failed requests~5% lost~5% lost~0.01% lost500× more reliable
Development timeDaysHoursMinutes100× faster

Real-world validation:

  • AWS Rust SDK: Uses similar pagination patterns
  • Stripe Rust client: Cursor-based pagination with retry logic
  • GitHub Octokit: Iterator-based API clients
  • Google Cloud SDK: Paginated resources with automatic retry

Production requirements met:

  • ✅ Memory efficient (constant for streaming, O(pages) for caching)
  • ✅ Rate limit compliant (token bucket algorithm)
  • ✅ Fault tolerant (exponential backoff retry)
  • ✅ Fast (lazy evaluation, early termination)
  • ✅ Flexible (multiple pagination strategies)
  • ✅ Type safe (generic with trait bounds)
  • ✅ Composable (standard Iterator trait)

This project teaches patterns used in production Rust SDK clients processing billions of API requests daily.


Build The Project

Milestone 1: Basic Paginated Iterator with Offset-Based Pagination

Goal: Create an iterator that fetches pages using offset-based pagination.

What to implement:

  • PaginatedIterator<T> that yields items of type T
  • Track current offset and page size
  • Fetch next page when current buffer is exhausted
  • Stop iteration when no more data

Architecture:

  • Structs: PaginatedIterator<T>, PageResponse<T>
  • Fields: offset: usize, page_size: usize, buffer: Vec<T>, buffer_index: usize, total: Option<usize>
  • Functions:
    • new(page_size: usize) -> Self - Create iterator
    • fetch_page(&self, offset: usize, limit: usize) -> Result<PageResponse<T>> - Fetch page
    • next() -> Option<T> - Iterate items

Starter Code:

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

/// Response from a paginated API endpoint
#[derive(Debug, Deserialize)]
pub struct PageResponse<T> {
    items: Vec<T>,
    total: Option<usize>,
    has_more: bool,
}

/// Iterator over paginated API results
pub struct PaginatedIterator<T> {
    url: String,
    offset: usize,
    page_size: usize,
    buffer: Vec<T>,
    buffer_index: usize,
    done: bool,
    _phantom: std::marker::PhantomData<T>,
}

impl<T> PaginatedIterator<T>
where
    T: for<'de> Deserialize<'de>,
{
    /// Create a new paginated iterator
    /// Role: Initialize with API endpoint and page size
    pub fn new(url: String, page_size: usize) -> Self {
        todo!("Initialize iterator state")
    }

    /// Fetch a page from the API
    /// Role: HTTP request with offset and limit parameters
    fn fetch_page(&mut self) -> Result<(), FetchError> {
        todo!("Make HTTP GET request, deserialize response, update buffer")
    }
}

#[derive(Debug)]
pub enum FetchError {
    Http(String),
    Deserialization(String),
}

impl<T> Iterator for PaginatedIterator<T>
where
    T: for<'de> Deserialize<'de>,
{
    type Item = T;

    /// Yield next item, fetching new page if needed
    /// Role: Transparent pagination - user sees flat stream
    fn next(&mut self) -> Option<Self::Item> {
        todo!("Return item from buffer or fetch next page")
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use serde::Deserialize;

    #[derive(Debug, Deserialize, PartialEq)]
    struct TestItem {
        id: u32,
        name: String,
    }

    // Mock HTTP server for testing
    fn setup_mock_server() -> mockito::ServerGuard {
        todo!("Setup mockito server with paginated responses")
    }

    #[test]
    fn test_basic_pagination() {
        let server = setup_mock_server();

        // Mock returns 3 pages with 2 items each
        let iter = PaginatedIterator::<TestItem>::new(
            server.url(),
            2 // page_size
        );

        let items: Vec<_> = iter.collect();
        assert_eq!(items.len(), 6);
    }

    #[test]
    fn test_empty_result() {
        let server = setup_mock_server();

        let iter = PaginatedIterator::<TestItem>::new(server.url(), 10);
        let items: Vec<_> = iter.collect();

        assert_eq!(items.len(), 0);
    }

    #[test]
    fn test_single_page() {
        let server = setup_mock_server();

        // Mock returns 1 page with 3 items
        let iter = PaginatedIterator::<TestItem>::new(server.url(), 10);
        let items: Vec<_> = iter.collect();

        assert_eq!(items.len(), 3);
    }

    #[test]
    fn test_lazy_evaluation() {
        let server = setup_mock_server();

        let mut iter = PaginatedIterator::<TestItem>::new(server.url(), 2);

        // Should only fetch first page initially
        let first = iter.next().unwrap();
        assert_eq!(first.id, 1);

        // Should fetch second page when buffer exhausted
        iter.next(); // id=2
        let third = iter.next().unwrap();
        assert_eq!(third.id, 3);
    }
}
}

Milestone 2: Cursor-Based Pagination Support

Goal: Support cursor-based pagination (used by GitHub, Stripe, etc.).

Why the previous milestone is not enough: Offset-based pagination has issues with data insertion/deletion during iteration. Cursors provide stable pagination.

What’s the improvement: Cursor-based pagination uses opaque tokens to mark positions, remaining stable even as underlying data changes. This is essential for real-time data sources where items are added/removed frequently.

Architecture:

  • Enums: PaginationStrategy (Offset, Cursor, PageNumber)
  • Fields: cursor: Option<String>, pagination_strategy: PaginationStrategy
  • Functions:
    • with_cursor_pagination(url: String, page_size: usize) -> Self - Create cursor-based iterator
    • fetch_cursor_page(&mut self) -> Result<()> - Fetch using cursor

Starter Code:

#![allow(unused)]
fn main() {
/// Pagination strategies supported by the iterator
#[derive(Debug, Clone)]
pub enum PaginationStrategy {
    Offset { offset: usize, limit: usize },
    Cursor { cursor: Option<String>, page_size: usize },
    PageNumber { page: usize, per_page: usize },
}

/// Cursor-based page response
#[derive(Debug, Deserialize)]
pub struct CursorPageResponse<T> {
    items: Vec<T>,
    next_cursor: Option<String>,
    has_more: bool,
}

/// Universal paginated iterator supporting multiple strategies
pub struct UniversalPaginatedIterator<T> {
    url: String,
    strategy: PaginationStrategy,
    buffer: Vec<T>,
    buffer_index: usize,
    done: bool,
    _phantom: std::marker::PhantomData<T>,
}

impl<T> UniversalPaginatedIterator<T>
where
    T: for<'de> Deserialize<'de>,
{
    /// Create iterator with cursor-based pagination
    /// Role: Support cursor tokens for stable pagination
    pub fn with_cursor(url: String, page_size: usize) -> Self {
        todo!("Initialize with Cursor strategy")
    }

    /// Create iterator with offset-based pagination
    /// Role: Support offset/limit parameters
    pub fn with_offset(url: String, page_size: usize) -> Self {
        todo!("Initialize with Offset strategy")
    }

    /// Fetch next page based on strategy
    /// Role: Dispatch to appropriate fetch method
    fn fetch_next_page(&mut self) -> Result<(), FetchError> {
        match &self.strategy {
            PaginationStrategy::Offset { .. } => self.fetch_offset_page(),
            PaginationStrategy::Cursor { .. } => self.fetch_cursor_page(),
            PaginationStrategy::PageNumber { .. } => self.fetch_page_number_page(),
        }
    }

    /// Fetch page using cursor
    /// Role: HTTP GET with cursor parameter
    fn fetch_cursor_page(&mut self) -> Result<(), FetchError> {
        todo!("Fetch page with cursor, update cursor from response")
    }

    fn fetch_offset_page(&mut self) -> Result<(), FetchError> {
        todo!("Fetch page with offset/limit")
    }

    fn fetch_page_number_page(&mut self) -> Result<(), FetchError> {
        todo!("Fetch page by page number")
    }
}

impl<T> Iterator for UniversalPaginatedIterator<T>
where
    T: for<'de> Deserialize<'de>,
{
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        todo!("Fetch pages using current strategy")
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cursor_pagination() {
        let server = setup_mock_cursor_server();

        let iter = UniversalPaginatedIterator::<TestItem>::with_cursor(
            server.url(),
            2
        );

        let items: Vec<_> = iter.collect();
        assert_eq!(items.len(), 6);
    }

    #[test]
    fn test_stable_cursor_pagination() {
        // Simulate data insertion during iteration
        let server = setup_dynamic_mock_server();

        let iter = UniversalPaginatedIterator::<TestItem>::with_cursor(
            server.url(),
            2
        );

        // Cursor-based pagination should not skip or duplicate items
        let items: Vec<_> = iter.collect();

        // Verify all items unique
        let ids: std::collections::HashSet<_> = items.iter().map(|item| item.id).collect();
        assert_eq!(ids.len(), items.len());
    }

    #[test]
    fn test_multiple_pagination_strategies() {
        let server = setup_mock_server();

        // Test all strategies on same endpoint
        let offset_items: Vec<_> = UniversalPaginatedIterator::<TestItem>::with_offset(
            server.url(),
            3
        ).collect();

        let cursor_items: Vec<_> = UniversalPaginatedIterator::<TestItem>::with_cursor(
            server.url(),
            3
        ).collect();

        // Both should return same items
        assert_eq!(offset_items.len(), cursor_items.len());
    }
}
}

Milestone 3: Rate Limiting and Retry Logic

Goal: Add rate limiting to respect API rate limits and retry transient failures.

Why the previous milestone is not enough: Production APIs have rate limits. Exceeding them causes 429 errors and IP bans. Network errors require retries.

What’s the improvement: Built-in rate limiting prevents exceeding API quotas. Exponential backoff retry logic handles transient failures (network glitches, temporary server errors). This makes the iterator production-ready and resilient to real-world network conditions.

Architecture:

  • Structs: RateLimiter, RetryPolicy
  • Fields: rate_limiter: RateLimiter, retry_policy: RetryPolicy
  • Functions:
    • with_rate_limit(requests_per_second: f64) - Configure rate limiting
    • with_retry_policy(max_attempts: usize, backoff: Duration) - Configure retries

Starter Code:

#![allow(unused)]
fn main() {
use std::time::{Duration, Instant};

/// Rate limiter using token bucket algorithm
pub struct RateLimiter {
    tokens: f64,
    max_tokens: f64,
    refill_rate: f64, // tokens per second
    last_refill: Instant,
}

impl RateLimiter {
    /// Create rate limiter
    /// Role: Allow up to `rate` requests per second
    pub fn new(rate: f64) -> Self {
        todo!("Initialize token bucket")
    }

    /// Acquire a token, blocking if necessary
    /// Role: Enforce rate limit
    pub fn acquire(&mut self) {
        todo!("Wait until token available")
    }

    /// Refill tokens based on elapsed time
    /// Role: Add tokens at configured rate
    fn refill(&mut self) {
        todo!("Calculate elapsed time, add tokens")
    }
}

/// Retry policy with exponential backoff
#[derive(Debug, Clone)]
pub struct RetryPolicy {
    max_attempts: usize,
    initial_backoff: Duration,
    max_backoff: Duration,
    multiplier: f64,
}

impl RetryPolicy {
    /// Create retry policy
    /// Role: Configure exponential backoff
    pub fn new(max_attempts: usize, initial_backoff: Duration) -> Self {
        RetryPolicy {
            max_attempts,
            initial_backoff,
            max_backoff: Duration::from_secs(60),
            multiplier: 2.0,
        }
    }

    /// Check if error is retryable
    /// Role: Determine if should retry based on error type
    pub fn should_retry(&self, error: &FetchError, attempt: usize) -> bool {
        todo!("Check error type and attempt count")
    }

    /// Calculate delay for attempt
    /// Role: Exponential backoff calculation
    pub fn delay_for_attempt(&self, attempt: usize) -> Duration {
        todo!("Calculate delay with exponential backoff")
    }
}

/// Paginated iterator with rate limiting and retries
///
/// Fields:
/// - rate_limiter: Option<RateLimiter>
/// - retry_policy: RetryPolicy
impl<T> UniversalPaginatedIterator<T>
where
    T: for<'de> Deserialize<'de>,
{
    /// Add rate limiting
    /// Role: Prevent exceeding API rate limits
    pub fn with_rate_limit(mut self, requests_per_second: f64) -> Self {
        todo!("Add rate limiter")
    }

    /// Add retry policy
    /// Role: Handle transient failures
    pub fn with_retries(mut self, max_attempts: usize, initial_backoff: Duration) -> Self {
        todo!("Configure retry policy")
    }

    /// Fetch with rate limiting and retries
    /// Role: Resilient HTTP request
    fn fetch_with_resilience(&mut self) -> Result<(), FetchError> {
        todo!("Apply rate limit, fetch with retries")
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{Duration, Instant};

    #[test]
    fn test_rate_limiter() {
        let mut limiter = RateLimiter::new(10.0); // 10 req/sec

        let start = Instant::now();

        // Acquire 20 tokens (should take ~1 second)
        for _ in 0..20 {
            limiter.acquire();
        }

        let elapsed = start.elapsed();
        assert!(elapsed >= Duration::from_millis(900));
        assert!(elapsed <= Duration::from_millis(1100));
    }

    #[test]
    fn test_retry_policy() {
        let policy = RetryPolicy::new(3, Duration::from_millis(100));

        assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
        assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(200));
        assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(400));
    }

    #[test]
    fn test_retry_on_transient_error() {
        let server = setup_flaky_mock_server(); // Returns errors first 2 times

        let iter = UniversalPaginatedIterator::<TestItem>::with_cursor(server.url(), 10)
            .with_retries(3, Duration::from_millis(10));

        let items: Vec<_> = iter.collect();

        // Should succeed after retries
        assert!(items.len() > 0);
    }

    #[test]
    fn test_rate_limiting_in_iteration() {
        let server = setup_mock_server();

        let iter = UniversalPaginatedIterator::<TestItem>::with_offset(server.url(), 2)
            .with_rate_limit(5.0); // 5 requests/sec

        let start = Instant::now();

        // 6 items across 3 pages = 3 requests
        let items: Vec<_> = iter.collect();

        let elapsed = start.elapsed();

        // Should take at least 400ms (3 requests / 5 req/sec = 0.6s, minus first instant)
        assert!(elapsed >= Duration::from_millis(350));
        assert_eq!(items.len(), 6);
    }
}
}

Milestone 4: Caching to Avoid Redundant Requests

Goal: Cache fetched pages to enable re-iteration without additional HTTP requests.

Why the previous milestone is not enough: Iterating multiple times (e.g., .clone().filter(...).collect()) re-fetches all pages, wasting API quota and time.

What’s the improvement: In-memory caching stores fetched pages. Subsequent iterations use cached data instead of HTTP requests. This is essential for exploratory data analysis where you might iterate multiple times with different filters.

Optimization focus: Speed and API quota conservation through caching.

Architecture:

  • Structs: CachedPaginatedIterator<T>
  • Fields: cache: Arc<Mutex<Vec<Vec<T>>>>, cache_position: usize
  • Functions:
    • with_cache(self) -> CachedPaginatedIterator<T> - Enable caching
    • reset(&mut self) - Reset to beginning
    • clone_iter(&self) -> Self - Clone for re-iteration

Starter Code:

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};

/// Cached paginated iterator for re-iteration without refetching
pub struct CachedPaginatedIterator<T>
where
    T: Clone,
{
    inner: UniversalPaginatedIterator<T>,
    cache: Arc<Mutex<Vec<Vec<T>>>>,
    cache_index: usize,
    item_index: usize,
    cache_complete: Arc<Mutex<bool>>,
}

impl<T> CachedPaginatedIterator<T>
where
    T: for<'de> Deserialize<'de> + Clone,
{
    /// Wrap iterator with caching
    /// Role: Enable re-iteration without refetching
    pub fn with_cache(inner: UniversalPaginatedIterator<T>) -> Self {
        todo!("Initialize cache structures")
    }

    /// Reset iterator to beginning
    /// Role: Re-iterate over cached data
    pub fn reset(&mut self) {
        todo!("Reset indices to start")
    }

    /// Clone iterator sharing same cache
    /// Role: Multiple iterators over same cached data
    pub fn clone_iter(&self) -> Self {
        todo!("Clone with shared cache reference")
    }
}

impl<T> Iterator for CachedPaginatedIterator<T>
where
    T: for<'de> Deserialize<'de> + Clone,
{
    type Item = T;

    /// Yield items from cache or fetch and cache new pages
    /// Role: Transparent caching
    fn next(&mut self) -> Option<Self::Item> {
        todo!("Check cache first, fetch and cache if needed")
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basic_caching() {
        let server = setup_request_counting_mock_server();

        let iter = UniversalPaginatedIterator::<TestItem>::with_cursor(server.url(), 2);
        let mut cached = CachedPaginatedIterator::with_cache(iter);

        // First iteration fetches from API
        let items1: Vec<_> = cached.by_ref().collect();
        let request_count_1 = server.request_count();

        // Reset and iterate again
        cached.reset();
        let items2: Vec<_> = cached.collect();
        let request_count_2 = server.request_count();

        // Should not make additional requests
        assert_eq!(request_count_1, request_count_2);
        assert_eq!(items1, items2);
    }

    #[test]
    fn test_clone_iter_shares_cache() {
        let server = setup_request_counting_mock_server();

        let iter = UniversalPaginatedIterator::<TestItem>::with_offset(server.url(), 3);
        let cached = CachedPaginatedIterator::with_cache(iter);

        // First iterator populates cache
        let _items1: Vec<_> = cached.clone_iter().collect();
        let request_count_1 = server.request_count();

        // Second iterator uses same cache
        let _items2: Vec<_> = cached.clone_iter().collect();
        let request_count_2 = server.request_count();

        // No additional requests
        assert_eq!(request_count_1, request_count_2);
    }

    #[test]
    fn test_partial_iteration_and_reset() {
        let server = setup_mock_server();

        let iter = UniversalPaginatedIterator::<TestItem>::with_cursor(server.url(), 2);
        let mut cached = CachedPaginatedIterator::with_cache(iter);

        // Partially iterate
        let first_3: Vec<_> = cached.by_ref().take(3).collect();
        assert_eq!(first_3.len(), 3);

        // Reset and get all
        cached.reset();
        let all: Vec<_> = cached.collect();

        assert!(all.len() > 3);
        assert_eq!(&all[..3], &first_3[..]);
    }
}
}

Milestone 5: Parallel Page Fetching

Goal: Prefetch multiple pages in parallel to reduce latency.

Why the previous milestone is not enough: Sequential page fetching wastes time waiting for network I/O. When processing item N, we could already be fetching page N+1.

What’s the improvement: Parallel prefetching uses background threads to fetch upcoming pages while the main iterator processes current data. This overlaps computation and I/O, dramatically reducing total time.

For a 100-page dataset with 50ms per request:

  • Sequential: 100 × 50ms = 5000ms
  • Parallel (prefetch 5): ~1000ms (5x speedup)

Optimization focus: Speed through parallel I/O and prefetching.

Architecture:

  • Structs: PrefetchingIterator<T>
  • Fields: prefetch_queue: Arc<Mutex<VecDeque<Vec<T>>>>, fetch_handle: Option<JoinHandle<()>>
  • Functions:
    • with_prefetch(self, prefetch_pages: usize) -> PrefetchingIterator<T> - Enable prefetching
    • spawn_prefetch_worker() - Background fetch thread

Starter Code:

#![allow(unused)]
fn main() {
use std::sync::mpsc::{channel, Sender, Receiver};
use std::thread::{self, JoinHandle};
use std::collections::VecDeque;

/// Prefetching iterator that fetches pages in background
pub struct PrefetchingIterator<T>
where
    T: Send + 'static,
{
    receiver: Receiver<Result<Vec<T>, FetchError>>,
    buffer: VecDeque<T>,
    fetch_handle: Option<JoinHandle<()>>,
    done: bool,
}

impl<T> PrefetchingIterator<T>
where
    T: for<'de> Deserialize<'de> + Send + 'static + Clone,
{
    /// Create iterator with prefetching
    /// Role: Overlap I/O with processing
    pub fn with_prefetch(
        base_iter: UniversalPaginatedIterator<T>,
        prefetch_pages: usize
    ) -> Self {
        todo!("Spawn background thread, set up channel")
    }

    /// Spawn worker thread that fetches pages ahead
    /// Role: Prefetch pages and send via channel
    fn spawn_fetch_worker(
        mut iter: UniversalPaginatedIterator<T>,
        sender: Sender<Result<Vec<T>, FetchError>>,
        prefetch_count: usize
    ) -> JoinHandle<()> {
        thread::spawn(move || {
            todo!("Fetch pages and send to channel")
        })
    }

    /// Refill buffer from prefetch queue
    /// Role: Load next prefetched page
    fn refill_buffer(&mut self) -> bool {
        todo!("Receive from channel, update buffer")
    }
}

impl<T> Iterator for PrefetchingIterator<T>
where
    T: Send + 'static,
{
    type Item = Result<T, FetchError>;

    fn next(&mut self) -> Option<Self::Item> {
        todo!("Yield from buffer, refill from prefetch queue")
    }
}

impl<T> Drop for PrefetchingIterator<T>
where
    T: Send + 'static,
{
    /// Clean up background thread
    /// Role: Ensure worker thread is joined
    fn drop(&mut self) {
        todo!("Join fetch handle")
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Instant;

    #[test]
    fn test_prefetch_correctness() {
        let server = setup_mock_server();

        let iter = UniversalPaginatedIterator::<TestItem>::with_cursor(server.url(), 2);
        let prefetch_iter = PrefetchingIterator::with_prefetch(iter, 3);

        let items: Result<Vec<_>, _> = prefetch_iter.collect();
        let items = items.unwrap();

        assert_eq!(items.len(), 6);
    }

    #[test]
    fn test_prefetch_performance() {
        let server = setup_slow_mock_server(Duration::from_millis(50)); // 50ms per page

        // Sequential
        let iter_seq = UniversalPaginatedIterator::<TestItem>::with_cursor(server.url(), 2);
        let start = Instant::now();
        let items_seq: Vec<_> = iter_seq.collect();
        let seq_time = start.elapsed();

        // With prefetch
        let iter_prefetch = UniversalPaginatedIterator::<TestItem>::with_cursor(server.url(), 2);
        let prefetch = PrefetchingIterator::with_prefetch(iter_prefetch, 5);
        let start = Instant::now();
        let items_prefetch: Result<Vec<_>, _> = prefetch.collect();
        let prefetch_time = start.elapsed();

        // Prefetch should be significantly faster
        println!("Sequential: {:?}, Prefetch: {:?}", seq_time, prefetch_time);
        assert!(prefetch_time < seq_time / 2);
        assert_eq!(items_seq.len(), items_prefetch.unwrap().len());
    }

    #[test]
    fn test_prefetch_handles_errors() {
        let server = setup_error_mock_server();

        let iter = UniversalPaginatedIterator::<TestItem>::with_cursor(server.url(), 2);
        let prefetch = PrefetchingIterator::with_prefetch(iter, 3);

        // Should propagate errors
        let result: Result<Vec<_>, _> = prefetch.collect();
        assert!(result.is_err());
    }
}
}

Milestone 6: Complete API Client with Builder Pattern

Goal: Combine all features into a ergonomic API client with builder pattern.

Why the previous milestone is not enough: Individual features work, but users need a unified, discoverable API. Builder pattern provides fluent interface.

What’s the improvement: Builder pattern makes configuration discoverable through IDE autocomplete. All features (pagination, rate limiting, retries, caching, prefetching) compose elegantly. This is production-ready API client design.

Architecture:

  • Structs: ApiClient, PaginatedRequestBuilder<T>
  • Functions:
    • ApiClient::new(base_url) - Create client
    • .paginated<T>(endpoint) - Start pagination builder
    • Builder methods: .page_size(), .with_cursor(), .rate_limit(), .retries(), .cache(), .prefetch()

Starter Code:

#![allow(unused)]
fn main() {
/// API client with fluent builder interface
pub struct ApiClient {
    base_url: String,
    http_client: reqwest::blocking::Client,
}

pub struct PaginatedRequestBuilder<T> {
    client: ApiClient,
    endpoint: String,
    page_size: usize,
    strategy: Option<PaginationStrategy>,
    rate_limit: Option<f64>,
    retry_policy: Option<RetryPolicy>,
    enable_cache: bool,
    prefetch_pages: Option<usize>,
    _phantom: std::marker::PhantomData<T>,
}

impl ApiClient {
    /// Create new API client
    /// Role: Initialize client with base URL
    pub fn new(base_url: impl Into<String>) -> Self {
        todo!("Create client with reqwest")
    }

    /// Start building a paginated request
    /// Role: Entry point for pagination builder
    pub fn paginated<T>(&self, endpoint: impl Into<String>) -> PaginatedRequestBuilder<T>
    where
        T: for<'de> Deserialize<'de>,
    {
        todo!("Create builder")
    }
}

impl<T> PaginatedRequestBuilder<T>
where
    T: for<'de> Deserialize<'de> + Send + Clone + 'static,
{
    /// Set page size
    /// Role: Configure items per page
    pub fn page_size(mut self, size: usize) -> Self {
        self.page_size = size;
        self
    }

    /// Use cursor-based pagination
    /// Role: Select pagination strategy
    pub fn cursor_based(mut self) -> Self {
        todo!("Set cursor strategy")
    }

    /// Use offset-based pagination
    /// Role: Select pagination strategy
    pub fn offset_based(mut self) -> Self {
        todo!("Set offset strategy")
    }

    /// Enable rate limiting
    /// Role: Respect API rate limits
    pub fn rate_limit(mut self, requests_per_second: f64) -> Self {
        self.rate_limit = Some(requests_per_second);
        self
    }

    /// Configure retries
    /// Role: Handle transient failures
    pub fn with_retries(mut self, max_attempts: usize, initial_backoff: Duration) -> Self {
        self.retry_policy = Some(RetryPolicy::new(max_attempts, initial_backoff));
        self
    }

    /// Enable caching
    /// Role: Allow re-iteration without refetching
    pub fn cached(mut self) -> Self {
        self.enable_cache = true;
        self
    }

    /// Enable prefetching
    /// Role: Parallel page fetching
    pub fn prefetch(mut self, pages: usize) -> Self {
        self.prefetch_pages = Some(pages);
        self
    }

    /// Build and execute the request
    /// Role: Construct iterator with all configured features
    pub fn execute(self) -> Box<dyn Iterator<Item = Result<T, FetchError>>> {
        todo!("Build iterator with all enabled features")
    }
}
}

Checkpoint Tests:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_builder_basic_usage() {
        let server = setup_mock_server();
        let client = ApiClient::new(server.url());

        let items: Result<Vec<_>, _> = client
            .paginated::<TestItem>("/api/items")
            .page_size(10)
            .offset_based()
            .execute()
            .collect();

        assert!(items.is_ok());
    }

    #[test]
    fn test_builder_with_all_features() {
        let server = setup_mock_server();
        let client = ApiClient::new(server.url());

        let items: Result<Vec<_>, _> = client
            .paginated::<TestItem>("/api/items")
            .page_size(5)
            .cursor_based()
            .rate_limit(10.0)
            .with_retries(3, Duration::from_millis(100))
            .cached()
            .prefetch(3)
            .execute()
            .collect();

        assert!(items.is_ok());
    }

    #[test]
    fn test_real_world_usage() {
        // Example: Fetch all users from GitHub API
        let client = ApiClient::new("https://api.github.com");

        let users: Vec<_> = client
            .paginated::<GitHubUser>("/users")
            .page_size(100)
            .rate_limit(60.0 / 3600.0) // GitHub: 60 req/hour
            .with_retries(3, Duration::from_secs(1))
            .prefetch(5)
            .execute()
            .take(500) // Get first 500 users
            .filter_map(Result::ok)
            .collect();

        println!("Fetched {} users", users.len());
    }
}
}

Complete Working Example

#![allow(unused)]
fn main() {
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::marker::PhantomData;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};

// =============================================================================
// Core types shared across milestones
// =============================================================================

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PageResponse<T> {
    pub items: Vec<T>,
    pub next_cursor: Option<String>,
    pub has_more: bool,
    pub total: Option<usize>,
}

#[derive(Debug, Clone)]
pub enum FetchError {
    Http(String),
    Deserialization(String),
    RateLimitExceeded,
}

impl fmt::Display for FetchError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FetchError::Http(msg) => write!(f, "http error: {}", msg),
            FetchError::Deserialization(msg) => write!(f, "deserialization error: {}", msg),
            FetchError::RateLimitExceeded => write!(f, "rate limit exceeded"),
        }
    }
}

impl std::error::Error for FetchError {}

// =============================================================================
// Rate limiting & retries
// =============================================================================

#[derive(Debug, Clone)]
pub struct RateLimiter {
    tokens: f64,
    max_tokens: f64,
    refill_rate: f64,
    last_refill: Instant,
}

impl RateLimiter {
    pub fn new(requests_per_second: f64) -> Self {
        let tokens = requests_per_second.max(1.0);
        Self {
            tokens,
            max_tokens: tokens,
            refill_rate: tokens,
            last_refill: Instant::now(),
        }
    }

    pub fn acquire(&mut self) {
        self.refill();
        while self.tokens < 1.0 {
            let missing = 1.0 - self.tokens;
            let wait_secs = missing / self.refill_rate;
            let wait = Duration::from_secs_f64(wait_secs).min(Duration::from_millis(50));
            thread::sleep(wait);
            self.refill();
        }
        self.tokens -= 1.0;
    }

    fn refill(&mut self) {
        let now = Instant::now();
        let elapsed = now.duration_since(self.last_refill).as_secs_f64();
        self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max_tokens);
        self.last_refill = now;
    }
}

#[derive(Debug, Clone)]
pub struct RetryPolicy {
    pub max_attempts: usize,
    pub initial_backoff: Duration,
    pub multiplier: f64,
    pub max_backoff: Duration,
}

impl RetryPolicy {
    pub fn new(max_attempts: usize, initial_backoff: Duration) -> Self {
        Self {
            max_attempts: max_attempts.max(1),
            initial_backoff,
            multiplier: 2.0,
            max_backoff: Duration::from_secs(2),
        }
    }

    pub fn default() -> Self {
        Self::new(3, Duration::from_millis(5))
    }

    pub fn delay_for_attempt(&self, attempt: usize) -> Duration {
        let factor = self.multiplier.powi(attempt as i32);
        let delay = self.initial_backoff.mul_f64(factor);
        if delay > self.max_backoff {
            self.max_backoff
        } else {
            delay
        }
    }
}

// =============================================================================
// Pagination strategies
// =============================================================================

#[derive(Debug, Clone)]
pub enum PaginationStrategy {
    Offset {
        offset: usize,
        page_size: usize,
    },
    Cursor {
        cursor: Option<String>,
        page_size: usize,
    },
    PageNumber {
        page: usize,
        per_page: usize,
    },
}

impl PaginationStrategy {
    fn page_size(&self) -> usize {
        match self {
            PaginationStrategy::Offset { page_size, .. } => *page_size,
            PaginationStrategy::Cursor { page_size, .. } => *page_size,
            PaginationStrategy::PageNumber { per_page, .. } => *per_page,
        }
    }

    fn current_params(&self) -> PaginationParams {
        match self {
            PaginationStrategy::Offset { offset, page_size } => PaginationParams::Offset {
                offset: *offset,
                limit: *page_size,
            },
            PaginationStrategy::Cursor { cursor, page_size } => PaginationParams::Cursor {
                cursor: cursor.clone(),
                limit: *page_size,
            },
            PaginationStrategy::PageNumber { page, per_page } => PaginationParams::PageNumber {
                page: *page,
                per_page: *per_page,
            },
        }
    }

    fn advance<T>(&mut self, response: &PageResponse<T>) {
        match self {
            PaginationStrategy::Offset { offset, .. } => {
                *offset += response.items.len();
            }
            PaginationStrategy::Cursor { cursor, .. } => {
                *cursor = response.next_cursor.clone();
            }
            PaginationStrategy::PageNumber { page, .. } => {
                *page += 1;
            }
        }
    }
}

#[derive(Debug, Clone)]
pub enum PaginationParams {
    Offset {
        offset: usize,
        limit: usize,
    },
    Cursor {
        cursor: Option<String>,
        limit: usize,
    },
    PageNumber {
        page: usize,
        per_page: usize,
    },
}

// =============================================================================
// Backend abstraction
// =============================================================================

pub trait PaginatedBackend: Send + Sync {
    fn fetch_page(
        &self,
        endpoint: &str,
        params: &PaginationParams,
    ) -> Result<PageResponse<Value>, FetchError>;
}

// =============================================================================
// Milestone 1 & 2: Universal iterator supporting multiple strategies
// =============================================================================

pub struct UniversalPaginatedIterator<T> {
    backend: Arc<dyn PaginatedBackend>,
    endpoint: String,
    strategy: PaginationStrategy,
    buffer: VecDeque<T>,
    more_pages_available: bool,
    rate_limiter: Option<RateLimiter>,
    retry_policy: RetryPolicy,
    _marker: PhantomData<T>,
}

impl<T> UniversalPaginatedIterator<T>
where
    T: DeserializeOwned + Send + 'static,
{
    pub fn new(
        backend: Arc<dyn PaginatedBackend>,
        endpoint: impl Into<String>,
        strategy: PaginationStrategy,
    ) -> Self {
        Self {
            backend,
            endpoint: endpoint.into(),
            strategy,
            buffer: VecDeque::new(),
            more_pages_available: true,
            rate_limiter: None,
            retry_policy: RetryPolicy::default(),
            _marker: PhantomData,
        }
    }

    pub fn with_rate_limit(mut self, requests_per_second: f64) -> Self {
        self.rate_limiter = Some(RateLimiter::new(requests_per_second));
        self
    }

    pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
        self.retry_policy = policy;
        self
    }

    pub fn page_size(&self) -> usize {
        self.strategy.page_size()
    }

    fn fetch_next_page(&mut self) -> Result<(), FetchError> {
        let params = self.strategy.current_params();
        let mut attempts = 0usize;
        loop {
            if let Some(limiter) = &mut self.rate_limiter {
                limiter.acquire();
            }
            match self.backend.fetch_page(&self.endpoint, &params) {
                Ok(response) => {
                    let mut next_buffer = VecDeque::with_capacity(response.items.len());
                    for value in response.items {
                        match serde_json::from_value::<T>(value) {
                            Ok(item) => next_buffer.push_back(item),
                            Err(err) => {
                                self.more_pages_available = false;
                                return Err(FetchError::Deserialization(err.to_string()));
                            }
                        }
                    }

                    self.buffer = next_buffer;
                    self.more_pages_available = response.has_more;
                    self.strategy.advance(&response);
                    return Ok(());
                }
                Err(err) => {
                    attempts += 1;
                    if attempts >= self.retry_policy.max_attempts {
                        self.more_pages_available = false;
                        return Err(err);
                    }
                    thread::sleep(self.retry_policy.delay_for_attempt(attempts - 1));
                }
            }
        }
    }
}

impl<T> Iterator for UniversalPaginatedIterator<T>
where
    T: DeserializeOwned + Send + 'static,
{
    type Item = Result<T, FetchError>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if let Some(item) = self.buffer.pop_front() {
                return Some(Ok(item));
            }

            if !self.more_pages_available {
                return None;
            }

            if let Err(err) = self.fetch_next_page() {
                return Some(Err(err));
            }

            if self.buffer.is_empty() && !self.more_pages_available {
                return None;
            }
        }
    }
}

// =============================================================================
// Milestone 4: Cached iterator
// =============================================================================

struct CachedShared<T> {
    cache: Mutex<Vec<T>>,
    inner: Mutex<Option<Box<dyn Iterator<Item = Result<T, FetchError>> + Send>>>,
    complete: Mutex<bool>,
}

pub struct CachedPaginatedIterator<T>
where
    T: Clone,
{
    shared: Arc<CachedShared<T>>,
    position: usize,
}

impl<T> CachedPaginatedIterator<T>
where
    T: Clone + Send + 'static,
{
    pub fn with_cache<I>(iterator: I) -> Self
    where
        I: Iterator<Item = Result<T, FetchError>> + Send + 'static,
    {
        Self {
            shared: Arc::new(CachedShared {
                cache: Mutex::new(Vec::new()),
                inner: Mutex::new(Some(Box::new(iterator))),
                complete: Mutex::new(false),
            }),
            position: 0,
        }
    }

    pub fn reset(&mut self) {
        self.position = 0;
    }

    pub fn clone_iter(&self) -> Self {
        Self {
            shared: Arc::clone(&self.shared),
            position: 0,
        }
    }

    fn next_from_inner(&mut self) -> Option<Result<T, FetchError>> {
        let mut inner_guard = self.shared.inner.lock().unwrap();
        if let Some(iter) = inner_guard.as_mut() {
            match iter.next() {
                Some(Ok(item)) => {
                    self.shared.cache.lock().unwrap().push(item.clone());
                    self.position += 1;
                    Some(Ok(item))
                }
                Some(Err(err)) => {
                    *self.shared.complete.lock().unwrap() = true;
                    Some(Err(err))
                }
                None => {
                    *self.shared.complete.lock().unwrap() = true;
                    None
                }
            }
        } else {
            *self.shared.complete.lock().unwrap() = true;
            None
        }
    }
}

impl<T> Iterator for CachedPaginatedIterator<T>
where
    T: Clone + Send + 'static,
{
    type Item = Result<T, FetchError>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if let Some(item) = {
                let cache = self.shared.cache.lock().unwrap();
                cache.get(self.position).cloned()
            } {
                self.position += 1;
                return Some(Ok(item));
            }

            if *self.shared.complete.lock().unwrap() {
                return None;
            }

            if let Some(result) = self.next_from_inner() {
                return Some(result);
            }
        }
    }
}

// =============================================================================
// Milestone 5: Prefetching iterator
// =============================================================================

pub struct PrefetchingIterator<T>
where
    T: Send + 'static,
{
    receiver: Receiver<Result<T, FetchError>>,
    fetch_handle: Option<thread::JoinHandle<()>>,
    done: bool,
}

impl<T> PrefetchingIterator<T>
where
    T: Send + 'static,
{
    pub fn with_prefetch<I>(iterator: I, buffered_pages: usize) -> Self
    where
        I: Iterator<Item = Result<T, FetchError>> + Send + 'static,
    {
        let capacity = buffered_pages.max(1) * 2;
        let (sender, receiver) = sync_channel(capacity);
        let handle = Self::spawn_fetch_worker(iterator, sender);
        Self {
            receiver,
            fetch_handle: Some(handle),
            done: false,
        }
    }

    fn spawn_fetch_worker<I>(
        mut iter: I,
        sender: SyncSender<Result<T, FetchError>>,
    ) -> thread::JoinHandle<()>
    where
        I: Iterator<Item = Result<T, FetchError>> + Send + 'static,
    {
        thread::spawn(move || {
            while let Some(item) = iter.next() {
                if sender.send(item).is_err() {
                    return;
                }
            }
        })
    }
}

impl<T> Iterator for PrefetchingIterator<T>
where
    T: Send + 'static,
{
    type Item = Result<T, FetchError>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.done {
            return None;
        }

        match self.receiver.recv() {
            Ok(item) => Some(item),
            Err(_) => {
                self.done = true;
                None
            }
        }
    }
}

impl<T> Drop for PrefetchingIterator<T>
where
    T: Send + 'static,
{
    fn drop(&mut self) {
        if let Some(handle) = self.fetch_handle.take() {
            let _ = handle.join();
        }
    }
}

// =============================================================================
// Milestone 6: API client builder
// =============================================================================

#[derive(Clone)]
pub struct ApiClient {
    base_url: String,
    backend: Arc<dyn PaginatedBackend>,
}

impl ApiClient {
    pub fn new(base_url: impl Into<String>) -> Self {
        Self {
            base_url: base_url.into(),
            backend: Arc::new(HttpBackend {}),
        }
    }

    pub fn with_backend(base_url: impl Into<String>, backend: Arc<dyn PaginatedBackend>) -> Self {
        Self {
            base_url: base_url.into(),
            backend,
        }
    }

    pub fn paginated<T>(&self, endpoint: impl Into<String>) -> PaginatedRequestBuilder<T>
    where
        T: DeserializeOwned + Clone + Send + 'static,
    {
        PaginatedRequestBuilder::new(
            self.base_url.clone(),
            endpoint.into(),
            Arc::clone(&self.backend),
        )
    }
}

struct HttpBackend;

impl PaginatedBackend for HttpBackend {
    fn fetch_page(
        &self,
        _endpoint: &str,
        _params: &PaginationParams,
    ) -> Result<PageResponse<Value>, FetchError> {
        Err(FetchError::Http(
            "HTTP backend not implemented in example".to_string(),
        ))
    }
}

#[derive(Debug, Clone, Copy)]
enum StrategyKind {
    Offset,
    Cursor,
    PageNumber,
}

pub struct PaginatedRequestBuilder<T> {
    base_url: String,
    endpoint: String,
    backend: Arc<dyn PaginatedBackend>,
    page_size: usize,
    rate_limit: Option<f64>,
    retry_policy: Option<RetryPolicy>,
    enable_cache: bool,
    prefetch_pages: Option<usize>,
    strategy: StrategyKind,
    _marker: PhantomData<T>,
}

impl<T> PaginatedRequestBuilder<T>
where
    T: DeserializeOwned + Clone + Send + 'static,
{
    fn new(base_url: String, endpoint: String, backend: Arc<dyn PaginatedBackend>) -> Self {
        Self {
            base_url,
            endpoint,
            backend,
            page_size: 50,
            rate_limit: None,
            retry_policy: None,
            enable_cache: false,
            prefetch_pages: None,
            strategy: StrategyKind::Offset,
            _marker: PhantomData,
        }
    }

    pub fn page_size(mut self, size: usize) -> Self {
        self.page_size = size.max(1);
        self
    }

    pub fn cursor_based(mut self) -> Self {
        self.strategy = StrategyKind::Cursor;
        self
    }

    pub fn offset_based(mut self) -> Self {
        self.strategy = StrategyKind::Offset;
        self
    }

    pub fn page_number_based(mut self) -> Self {
        self.strategy = StrategyKind::PageNumber;
        self
    }

    pub fn rate_limit(mut self, requests_per_second: f64) -> Self {
        self.rate_limit = Some(requests_per_second);
        self
    }

    pub fn with_retries(mut self, max_attempts: usize, backoff: Duration) -> Self {
        self.retry_policy = Some(RetryPolicy::new(max_attempts, backoff));
        self
    }

    pub fn cached(mut self) -> Self {
        self.enable_cache = true;
        self
    }

    pub fn prefetch(mut self, pages: usize) -> Self {
        self.prefetch_pages = Some(pages.max(1));
        self
    }

    fn build_strategy(&self) -> PaginationStrategy {
        match self.strategy {
            StrategyKind::Offset => PaginationStrategy::Offset {
                offset: 0,
                page_size: self.page_size,
            },
            StrategyKind::Cursor => PaginationStrategy::Cursor {
                cursor: None,
                page_size: self.page_size,
            },
            StrategyKind::PageNumber => PaginationStrategy::PageNumber {
                page: 0,
                per_page: self.page_size,
            },
        }
    }

    pub fn execute(self) -> Box<dyn Iterator<Item = Result<T, FetchError>> + Send> {
        let url = format!("{}{}", self.base_url, self.endpoint);
        let mut base_iter =
            UniversalPaginatedIterator::new(Arc::clone(&self.backend), url, self.build_strategy());

        if let Some(policy) = self.retry_policy {
            base_iter = base_iter.with_retry_policy(policy);
        }

        if let Some(rate) = self.rate_limit {
            base_iter = base_iter.with_rate_limit(rate);
        }

        let mut iterator: Box<dyn Iterator<Item = Result<T, FetchError>> + Send> =
            Box::new(base_iter);

        if self.enable_cache {
            iterator = Box::new(CachedPaginatedIterator::with_cache(iterator));
        }

        if let Some(pages) = self.prefetch_pages {
            iterator = Box::new(PrefetchingIterator::with_prefetch(
                iterator,
                pages * self.page_size,
            ));
        }

        iterator
    }
}

// =============================================================================
// Tests covering all milestones
// =============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
    struct TestItem {
        id: usize,
        name: String,
    }

    fn make_items(count: usize) -> Vec<TestItem> {
        (0..count)
            .map(|id| TestItem {
                id,
                name: format!("Item {}", id),
            })
            .collect()
    }

    fn register_dataset(
        backend: &Arc<MockBackend>,
        base_url: &str,
        endpoint: &str,
        items: &[TestItem],
    ) -> String {
        let url = format!("{}{}", base_url, endpoint);
        backend.add_endpoint(&url, items);
        url
    }

    #[derive(Default)]
    struct MockBackend {
        data: Mutex<HashMap<String, Vec<Value>>>,
        request_counts: Mutex<HashMap<String, usize>>,
        failures: Mutex<HashMap<String, usize>>,
        delay: Mutex<Duration>,
    }

    impl MockBackend {
        fn new() -> Self {
            Self::default()
        }

        fn add_endpoint<T>(&self, endpoint: &str, items: &[T])
        where
            T: Serialize,
        {
            let values = items
                .iter()
                .map(|item| serde_json::to_value(item).unwrap())
                .collect::<Vec<_>>();
            self.data
                .lock()
                .unwrap()
                .insert(endpoint.to_string(), values);
        }

        fn request_count(&self, endpoint: &str) -> usize {
            *self
                .request_counts
                .lock()
                .unwrap()
                .get(endpoint)
                .unwrap_or(&0)
        }

        fn set_failures(&self, endpoint: &str, failures: usize) {
            self.failures
                .lock()
                .unwrap()
                .insert(endpoint.to_string(), failures);
        }

        fn set_delay(&self, delay: Duration) {
            *self.delay.lock().unwrap() = delay;
        }
    }

    impl PaginatedBackend for MockBackend {
        fn fetch_page(
            &self,
            endpoint: &str,
            params: &PaginationParams,
        ) -> Result<PageResponse<Value>, FetchError> {
            thread::sleep(*self.delay.lock().unwrap());

            if let Some(counter) = self.failures.lock().unwrap().get_mut(endpoint) {
                if *counter > 0 {
                    *counter -= 1;
                    return Err(FetchError::Http("forced failure".into()));
                }
            }

            let mut counts = self.request_counts.lock().unwrap();
            *counts.entry(endpoint.to_string()).or_default() += 1;
            drop(counts);

            let storage = self.data.lock().unwrap();
            let dataset = storage.get(endpoint).cloned().unwrap_or_default();
            drop(storage);

            let len = dataset.len();
            let (offset, limit) = match params {
                PaginationParams::Offset { offset, limit } => (*offset, *limit),
                PaginationParams::Cursor { cursor, limit } => {
                    let parsed = cursor
                        .as_ref()
                        .and_then(|s| s.parse::<usize>().ok())
                        .unwrap_or(0);
                    (parsed, *limit)
                }
                PaginationParams::PageNumber { page, per_page } => (page * per_page, *per_page),
            };

            let start = offset.min(len);
            let end = (offset + limit).min(len);
            let items = dataset[start..end].to_vec();
            let has_more = end < len;
            let next_cursor = match params {
                PaginationParams::Cursor { .. } => has_more.then_some(end.to_string()),
                _ => None,
            };

            Ok(PageResponse {
                items,
                next_cursor,
                has_more,
                total: Some(len),
            })
        }
    }

    #[test]
    fn basic_offset_pagination() {
        let backend = Arc::new(MockBackend::new());
        let base_url = "mock://api";
        let endpoint = register_dataset(&backend, base_url, "/items", &make_items(6));

        let iter = UniversalPaginatedIterator::new(
            backend,
            endpoint,
            PaginationStrategy::Offset {
                offset: 0,
                page_size: 2,
            },
        );

        let collected: Vec<_> = iter.map(Result::unwrap).collect();
        assert_eq!(collected.len(), 6);
        assert_eq!(collected[0].id, 0);
    }

    #[test]
    fn cursor_based_strategy() {
        let backend = Arc::new(MockBackend::new());
        let base_url = "mock://api";
        let endpoint = register_dataset(&backend, base_url, "/cursor", &make_items(5));

        let iter = UniversalPaginatedIterator::new(
            backend,
            endpoint,
            PaginationStrategy::Cursor {
                cursor: None,
                page_size: 2,
            },
        );

        let collected: Vec<_> = iter.map(Result::unwrap).collect();
        assert_eq!(collected.len(), 5);
        assert_eq!(collected.last().unwrap().id, 4);
    }

    #[test]
    fn rate_limiting_introduces_delay() {
        let backend = Arc::new(MockBackend::new());
        let base_url = "mock://api";
        let endpoint = register_dataset(&backend, base_url, "/slow", &make_items(6));

        let mut iter = UniversalPaginatedIterator::new(
            backend,
            endpoint,
            PaginationStrategy::Offset {
                offset: 0,
                page_size: 2,
            },
        )
        .with_rate_limit(2.0);

        let start = Instant::now();
        let _: Vec<_> = iter.by_ref().map(Result::unwrap).collect();
        let elapsed = start.elapsed();
        assert!(elapsed >= Duration::from_millis(400));
    }

    #[test]
    fn retry_policy_recovers() {
        let backend = Arc::new(MockBackend::new());
        let base_url = "mock://api";
        let endpoint = register_dataset(&backend, base_url, "/retry", &make_items(4));
        backend.set_failures(&endpoint, 2);

        let iter = UniversalPaginatedIterator::new(
            backend.clone(),
            endpoint.clone(),
            PaginationStrategy::Offset {
                offset: 0,
                page_size: 2,
            },
        )
        .with_retry_policy(RetryPolicy::new(5, Duration::from_millis(1)));

        let items: Vec<_> = iter.map(Result::unwrap).collect();
        assert_eq!(items.len(), 4);
        assert!(backend.request_count(&endpoint) >= 3);
    }

    #[test]
    fn caching_allows_reset() {
        let backend = Arc::new(MockBackend::new());
        let base_url = "mock://api";
        let endpoint = register_dataset(&backend, base_url, "/cache", &make_items(5));

        let iter = UniversalPaginatedIterator::new(
            backend.clone(),
            endpoint.clone(),
            PaginationStrategy::Offset {
                offset: 0,
                page_size: 2,
            },
        );

        let mut cached = CachedPaginatedIterator::with_cache(iter);
        let first: Vec<_> = cached.by_ref().map(Result::unwrap).collect();
        let count_after_first = backend.request_count(&endpoint);
        cached.reset();
        let second: Vec<_> = cached.by_ref().map(Result::unwrap).collect();

        assert_eq!(first, second);
        assert_eq!(count_after_first, backend.request_count(&endpoint));
    }

    #[test]
    fn caching_clone_shares_data() {
        let backend = Arc::new(MockBackend::new());
        let base_url = "mock://api";
        let endpoint = register_dataset(&backend, base_url, "/cache-clone", &make_items(4));

        let iter = UniversalPaginatedIterator::new(
            backend.clone(),
            endpoint.clone(),
            PaginationStrategy::Offset {
                offset: 0,
                page_size: 2,
            },
        );

        let cached = CachedPaginatedIterator::with_cache(iter);
        let mut iter1 = cached.clone_iter();
        let mut iter2 = cached.clone_iter();

        let collected1: Vec<_> = iter1.by_ref().map(Result::unwrap).collect();
        let count_after_first = backend.request_count(&endpoint);
        let collected2: Vec<_> = iter2.map(Result::unwrap).collect();

        assert_eq!(collected1, collected2);
        assert_eq!(count_after_first, backend.request_count(&endpoint));
    }

    #[test]
    fn prefetch_improves_latency() {
        let backend = Arc::new(MockBackend::new());
        backend.set_delay(Duration::from_millis(40));
        let base_url = "mock://api";
        let endpoint = register_dataset(&backend, base_url, "/prefetch", &make_items(30));

        let iter_seq = UniversalPaginatedIterator::new(
            backend.clone(),
            endpoint.clone(),
            PaginationStrategy::Offset {
                offset: 0,
                page_size: 5,
            },
        );

        let start_seq = Instant::now();
        let _: Vec<_> = iter_seq.map(Result::unwrap).collect();
        let seq_time = start_seq.elapsed();

        let iter_prefetch = UniversalPaginatedIterator::new(
            backend,
            endpoint,
            PaginationStrategy::Offset {
                offset: 0,
                page_size: 5,
            },
        );

        let mut prefetched = PrefetchingIterator::with_prefetch(iter_prefetch, 3);
        let start_prefetch = Instant::now();
        let _: Vec<_> = prefetched
            .by_ref()
            .map(|item| {
                thread::sleep(Duration::from_millis(5));
                item.unwrap()
            })
            .collect();
        let prefetch_time = start_prefetch.elapsed();

        assert!(prefetch_time < seq_time);
    }

    #[test]
    fn builder_basic_usage() {
        let backend = Arc::new(MockBackend::new());
        let base_url = "mock://api";
        register_dataset(&backend, base_url, "/builder", &make_items(9));
        let client = ApiClient::with_backend(base_url, backend);

        let items: Vec<_> = client
            .paginated::<TestItem>("/builder")
            .page_size(3)
            .offset_based()
            .execute()
            .map(Result::unwrap)
            .collect();

        assert_eq!(items.len(), 9);
    }

    #[test]
    fn builder_with_all_features() {
        let backend = Arc::new(MockBackend::new());
        backend.set_delay(Duration::from_millis(5));
        let base_url = "mock://api";
        register_dataset(&backend, base_url, "/builder-full", &make_items(12));
        backend.set_failures(&format!("{}{}", base_url, "/builder-full"), 1);
        let client = ApiClient::with_backend(base_url, backend);

        let items: Vec<_> = client
            .paginated::<TestItem>("/builder-full")
            .page_size(4)
            .cursor_based()
            .rate_limit(20.0)
            .with_retries(3, Duration::from_millis(2))
            .cached()
            .prefetch(2)
            .execute()
            .map(Result::unwrap)
            .collect();

        assert_eq!(items.len(), 12);
    }
}

}