Lesson 9 / 25
Designing Breakers and Combining Patterns
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.
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.
Quick check: 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
- 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.