Isaac.

patterns

Polly Resilience Patterns

Implement resilience with retry, circuit breaker, and bulkhead patterns.

By Emem IsaacApril 11, 20243 min read
#polly#resilience#circuit breaker#retry#fault tolerance
Share:

A Simple Analogy

Polly is like insurance for your API calls. When something fails, Polly automatically retries, stops calling broken services, and isolates failures to prevent cascading problems.


Why Polly?

  • Resilience: Auto-recover from transient failures
  • Circuit breaker: Stop calling failing services
  • Retry logic: With exponential backoff
  • Bulkhead: Isolate resource consumption
  • Fallback: Provide alternative responses

Retry Policy

var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: attempt => 
            TimeSpan.FromSeconds(Math.Pow(2, attempt)), // 1s, 2s, 4s
        onRetry: (outcome, timespan, retryCount, context) =>
        {
            logger.LogWarning(
                $"Retry {retryCount} after {timespan.TotalSeconds}s. Reason: {outcome.Exception?.Message}");
        });

var response = await retryPolicy.ExecuteAsync(async () =>
{
    return await httpClient.GetAsync("https://api.example.com/data");
});

Circuit Breaker

var circuitBreakerPolicy = Policy
    .Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 5,
        durationOfBreak: TimeSpan.FromSeconds(30),
        onBreak: (outcome, timespan) =>
        {
            logger.LogError($"Circuit breaker opened for {timespan.TotalSeconds}s");
        },
        onReset: () =>
        {
            logger.LogInformation("Circuit breaker reset");
        });

try
{
    var response = await circuitBreakerPolicy.ExecuteAsync(async () =>
    {
        return await httpClient.GetAsync("https://failing-api.com/data");
    });
}
catch (BrokenCircuitException)
{
    logger.LogError("Circuit breaker is open. Service is down.");
}

Bulkhead Isolation

var bulkheadPolicy = Policy.BulkheadAsync(
    maxParallelization: 10,
    maxQueuingActions: 20,
    onBulkheadRejectedExecutionAsync: context =>
    {
        logger.LogWarning("Bulkhead queue is full. Request rejected.");
        return Task.CompletedTask;
    });

try
{
    await bulkheadPolicy.ExecuteAsync(async () =>
    {
        await Task.Delay(1000); // Simulates work
    });
}
catch (BulkheadRejectedException)
{
    // Handle rejection
}

Combined Policies (Wrap)

// Retry -> Circuit Breaker -> Bulkhead
var policy = Policy.WrapAsync(
    retryPolicy,
    circuitBreakerPolicy,
    bulkheadPolicy);

var response = await policy.ExecuteAsync(async () =>
{
    return await httpClient.GetAsync("https://api.example.com/data");
});

Fallback Policy

var fallbackPolicy = Policy<HttpResponseMessage>
    .Handle<HttpRequestException>()
    .OrResult(r => !r.IsSuccessStatusCode)
    .FallbackAsync(
        fallbackAction: async () =>
        {
            logger.LogWarning("Using fallback response from cache");
            var cached = await cache.GetAsync("api-response");
            return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
            {
                Content = new StringContent(cached)
            };
        },
        onFallbackAsync: (outcome, context) =>
        {
            logger.LogError($"Fallback triggered: {outcome.Exception?.Message}");
            return Task.CompletedTask;
        });

var response = await fallbackPolicy.ExecuteAsync(async () =>
{
    return await httpClient.GetAsync("https://api.example.com/data");
});

HttpClientFactory Integration

builder.Services.AddHttpClient<IPaymentClient, PaymentClient>()
    .AddTransientHttpErrorPolicy()
    .WaitAndRetryAsync(3, retryAttempt =>
        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

builder.Services.AddHttpClient<IOrderClient, OrderClient>()
    .AddTransientHttpErrorPolicy()
    .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient<IUserClient, UserClient>()
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

Advanced: Async Bulkhead

var asyncBulkheadPolicy = Policy.BulkheadAsync(
    maxParallelization: 10,
    maxQueuingActions: 20);

var database = new AsyncBulkheadPolicy();
var externalApi = new AsyncBulkheadPolicy();

// Keep database and API calls isolated
var dbPolicy = asyncBulkheadPolicy;
var apiPolicy = asyncBulkheadPolicy;

// Even if API is overloaded, database calls proceed

Best Practices

  1. Use defaults: Start with retry and circuit breaker
  2. Monitor policies: Track rejections and failures
  3. Set realistic timeouts: Avoid cascading waits
  4. Test failure scenarios: Verify fallback works
  5. Document policies: Explain why each exists

Related Concepts

  • Hystrix (Java equivalent)
  • Service mesh patterns
  • Distributed tracing
  • Health checks

Summary

Polly provides resilience patterns for distributed systems. Combine retry, circuit breaker, and bulkhead policies to build systems that gracefully handle failures and failures.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours