Table of Contents

Retry

The Retry module provides configurable retry policies with backoff strategies. It integrates with Result<T, TError> — retries are driven by failures and produce a final Result.

Basic Usage

var result = Retry.WithMaxAttempts(3)
    .Execute(() => CallService());
// Retries up to 3 times, returns first Success or last Failure

Builder API

Build retry policies fluently:

var result = await Retry
    .WithMaxAttempts(5)
    .WithBackoff(Backoff.Exponential(TimeSpan.FromMilliseconds(100)))
    .WithRetryWhen(error => error is ExternalServiceError)
    .OnRetry((attempt, error) =>
        logger.LogWarning("Attempt {Attempt} failed: {Error}", attempt, error.Message))
    .ExecuteAsync(() => FetchFromApiAsync());

Policy Options

Method Description
WithMaxAttempts(int) Maximum number of attempts (at least 1)
WithBackoff(Func<int, TimeSpan>) Delay strategy between attempts
WithRetryWhen(Func<Error, bool>) Predicate to filter retryable errors
OnRetry(Action<int, Error>) Callback for logging/observability

Backoff Strategies

The Backoff class provides factory methods for common delay patterns:

// No delay between retries
Backoff.None

// Same delay every time
Backoff.Constant(TimeSpan.FromSeconds(1))

// Linearly increasing: initial + (attempt - 1) * increment
Backoff.Linear(
    initial:   TimeSpan.FromMilliseconds(100),
    increment: TimeSpan.FromMilliseconds(200))
// Delays: 100ms, 300ms, 500ms, 700ms, ...

// Exponentially increasing: initial * multiplier^(attempt - 1)
Backoff.Exponential(TimeSpan.FromMilliseconds(100))
// Delays: 100ms, 200ms, 400ms, 800ms, ...

// Exponential with a cap
Backoff.Exponential(
    initial:    TimeSpan.FromMilliseconds(100),
    multiplier: 2.0,
    maxDelay:   TimeSpan.FromSeconds(5))
// Delays: 100ms, 200ms, 400ms, ..., capped at 5s

Sync and Async

Both synchronous and asynchronous execution are supported:

// Synchronous
Result<Data, Error> result = policy.Execute(() => LoadData());

// Asynchronous
Result<Data, Error> result = await policy.ExecuteAsync(() => LoadDataAsync());

Selective Retry

Only retry specific error types:

var result = await Retry.WithMaxAttempts(3)
    .WithRetryWhen(error => error is ExternalServiceError or InternalError)
    .ExecuteAsync(() => CallExternalServiceAsync());
// ValidationError or NotFoundError will NOT be retried

Real-World Example

Using the DarkPeak.Functional.Http extensions, HTTP calls already return Result<T, Error> — no manual mapping needed:

var result = await Retry
    .WithMaxAttempts(5)
    .WithBackoff(Backoff.Exponential(
        TimeSpan.FromMilliseconds(200),
        multiplier: 2.0,
        maxDelay: TimeSpan.FromSeconds(10)))
    .WithRetryWhen(error => error is ExternalServiceError or HttpRequestError)
    .OnRetry((attempt, error) =>
        logger.LogWarning(
            "API call failed (attempt {Attempt}): {Error}",
            attempt, error.Message))
    .ExecuteAsync(() =>
        httpClient.GetResultAsync<Data>("/api/data"));

Composition with Circuit Breaker

Place a circuit breaker inside the retry to protect against sustained failures:

var breaker = CircuitBreaker
    .WithFailureThreshold(3)
    .WithResetTimeout(TimeSpan.FromSeconds(30))
    .WithBreakWhen(error => error is ExternalServiceError);

var result = await Retry
    .WithMaxAttempts(5)
    .WithBackoff(Backoff.Exponential(TimeSpan.FromMilliseconds(500)))
    .WithRetryWhen(error =>
        error is ExternalServiceError or CircuitBreakerOpenError)
    .ExecuteAsync(() =>
        breaker.ExecuteAsync(
            () => httpClient.GetResultAsync<Data>("/api/data")));

Composition with MemoizeResult

Cache successful results so retries only happen on the first call:

var cachedFetch = MemoizeResult.FuncAsync<string, Data, Error>(
    endpoint => Retry
        .WithMaxAttempts(3)
        .WithBackoff(Backoff.Exponential(TimeSpan.FromMilliseconds(200)))
        .ExecuteAsync(() =>
            httpClient.GetResultAsync<Data>(endpoint)),
    opts => opts.WithExpiration(TimeSpan.FromMinutes(5)));

var result = await cachedFetch("/api/data");