# Designing Breakers and Combining Patterns — Rate Limiting, Circuit Breakers & Resilience Patterns

Source: https://www.skillbyai.com/en/resilience-patterns/c-design

> Scope breakers correctly and order retries, breakers and timeouts.

## Scope, failure definition and order

A breaker should guard **one dependency** (or one endpoint of it), not a whole set: a breaker shared by the payments and email clients would block emails because payments failed. Decide carefully **what counts as failure**: timeouts, connection errors and 5xx responses usually count; client errors such as 400 or 404 usually do not, because they say nothing about the dependency's health. Combine patterns in a sensible order. A common arrangement from outside in is: **retry → circuit breaker → timeout → bulkhead → call**. The timeout bounds each attempt; the breaker sees every attempt's result and short-circuits when the dependency is clearly down; the retry wraps everything but should treat an open-circuit error as **not retryable**, otherwise it spins uselessly. Libraries apply a default order (Resilience4j's Spring aspects, for instance, put retry outermost), so check the documentation instead of assuming.

## A Polly v8 pipeline in .NET

Strategies are listed outermost first: retry, then breaker, then per-attempt timeout.

```cs
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
    {
        MaxRetryAttempts = 2,
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        Delay = TimeSpan.FromMilliseconds(100),
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .HandleResult(r => (int)r.StatusCode >= 500)
    })
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
    {
        FailureRatio = 0.5,
        MinimumThroughput = 20,
        SamplingDuration = TimeSpan.FromSeconds(30),
        BreakDuration = TimeSpan.FromSeconds(30)
    })
    .AddTimeout(TimeSpan.FromMilliseconds(800))
    .Build();

var response = await pipeline.ExecuteAsync(
    async ct => await http.GetAsync("https://payments.internal/health", ct), cancellationToken);
```

## Do not count client errors as failures

If a bug in your code sends malformed requests and the dependency correctly replies 400, opening the breaker would block valid requests too. Only count errors that indicate the dependency is unhealthy.

**Quiz:** Why should a retry policy usually not retry when a circuit breaker reports it is open?

- [ ] Open-circuit errors are permanent bugs
- [ ] Retries cannot be combined with breakers
- [x] The breaker has decided the dependency is unhealthy; retrying immediately just spins and wastes time
- [ ] It would close the breaker

*Answer:* The breaker has decided the dependency is unhealthy; retrying immediately just spins and wastes time. An open breaker fails fast by design; retrying it adds latency without any chance of success until it half-opens.
