Advanced Rust Design Patterns

Strategy Pattern with Traits

The Strategy pattern decouples an algorithm or policy (such as retry backoff, compression, load balancing, or formatting) from the host component that executes it, allowing callers to plug in different algorithms interchangeably. In Rust, the Strategy pattern is expressed through traits, supporting both zero-cost compile-time injection (S: Strategy) and runtime-swappable trait objects (Box<dyn Strategy>).

Host Component (`HttpClient<R: RetryStrategy>`)
        |
        +-- Compile-Time Strategy (`R: RetryStrategy = ExponentialBackoff`)
        |   ---> Monomorphized & inlined with 0 bytes of vtable overhead!
        |
        +-- Runtime Strategy (`Box<dyn RetryStrategy>`)
        |   ---> Swappable at runtime from a config file or CLI flag!
        |
        +-- Closure Strategy (`impl FnMut(u32) -> Option<Duration>`)
            ---> Ad-hoc one-liner strategy without defining a new struct!

Implementing a Strategy Trait with a Blanket Closure Implementation

A powerful Rust design idiom is to define a clean domain trait (pub trait BackoffPolicy) and also provide a blanket implementation for any closure F: Fn(u32) -> Option<Duration>! That way:

  • Standard strategies (FixedBackoff, ExponentialBackoff) are reusable named structs.
  • Callers who want a quick custom rule can pass an inline closure (|attempt| Some(Duration::from_millis(attempt * 50))) directly!
use std::time::Duration;

pub trait BackoffPolicy {
    /// Returns `Some(delay)` before retry `attempt` (1-indexed), or `None` to stop retrying.
    fn next_delay(&self, attempt: u32) -> Option<Duration>;
}

/// Blanket implementation: ANY closure `Fn(u32) -> Option<Duration>` is automatically a `BackoffPolicy`!
impl<F> BackoffPolicy for F
where
    F: Fn(u32) -> Option<Duration>,
{
    fn next_delay(&self, attempt: u32) -> Option<Duration> {
        self(attempt)
    }
}

/// Strategy 1: Exponential backoff capped at `max_attempts`.
#[derive(Debug, Clone, Copy)]
pub struct ExponentialBackoff {
    pub base_ms: u64,
    pub max_attempts: u32,
}

impl BackoffPolicy for ExponentialBackoff {
    fn next_delay(&self, attempt: u32) -> Option<Duration> {
        if attempt > self.max_attempts {
            return None;
        }
        let multiplier = 1u64.checked_shl(attempt.saturating_sub(1)).unwrap_or(u64::MAX);
        Some(Duration::from_millis(self.base_ms.saturating_mul(multiplier)))
    }
}

/// Strategy 2: No retries (fail immediately).
#[derive(Debug, Clone, Copy, Default)]
pub struct NoRetry;

impl BackoffPolicy for NoRetry {
    fn next_delay(&self, _attempt: u32) -> Option<Duration> {
        None
    }
}

/// Host runner parameterized over any `P: BackoffPolicy` (defaults to `ExponentialBackoff`).
pub fn collect_retry_schedule<P: BackoffPolicy>(policy: &P) -> Vec<Duration> {
    let mut delays = Vec::new();
    for attempt in 1..=10 {
        match policy.next_delay(attempt) {
            Some(d) => delays.push(d),
            None => break,
        }
    }
    delays
}

fn main() {
    // 1. Named struct strategy:
    let exp = ExponentialBackoff {
        base_ms: 100,
        max_attempts: 3,
    };
    let schedule = collect_retry_schedule(&exp);
    assert_eq!(
        schedule,
        vec![
            Duration::from_millis(100),
            Duration::from_millis(200),
            Duration::from_millis(400),
        ]
    );

    // 2. Ad-hoc closure strategy (enabled by the blanket impl!):
    let custom_schedule = collect_retry_schedule(&|attempt| {
        (attempt <= 2).then(|| Duration::from_secs(u64::from(attempt)))
    });
    assert_eq!(
        custom_schedule,
        vec![Duration::from_secs(1), Duration::from_secs(2)]
    );

    println!("Exponential schedule: {schedule:?}, Closure schedule: {custom_schedule:?}");
}
Exponential schedule: [100ms, 200ms, 400ms], Closure schedule: [1s, 2s]

Default Generic Type Parameters on Strategy Hosts

When declaring a host struct pub struct Client<P = ExponentialBackoff> { policy: P }, supplying a default type parameter (= ExponentialBackoff) keeps the type signature simple (let client: Client = ...;) for 95% of users who want the default strategy, while still allowing advanced users to substitute Client<CustomPolicy> with zero vtable overhead.