Resilience patterns (Circuit Breaker) for Railway Oriented Programming - extends Voyager.Common.Results with stateful fault tolerance patterns
$ dotnet add package Voyager.Common.ResilienceResilience patterns for Railway Oriented Programming - Advanced failure handling extensions for Voyager.Common.Results.
Prevent cascading failures and improve system reliability with the Circuit Breaker pattern integrated into the Result monad.
Supports .NET Framework 4.8, .NET 6.0, and .NET 8.0 🚀
# Install both packages
dotnet add package Voyager.Common.Results
dotnet add package Voyager.Common.Resilience
using Voyager.Common.Results;
using Voyager.Common.Resilience;
// Create a circuit breaker policy
var circuitBreaker = new CircuitBreakerPolicy(
failureThreshold: 5, // Open after 5 consecutive failures
openTimeout: TimeSpan.FromSeconds(30), // Stay open for 30 seconds
halfOpenMaxAttempts: 3 // Allow 3 recovery test attempts
);
// Execute operations through the circuit breaker
var result = await GetUser(userId)
.BindWithCircuitBreakerAsync(
user => CallExternalApiAsync(user),
circuitBreaker
);
// Handle results including circuit breaker state
var message = result.Match(
onSuccess: data => $"Success: {data}",
onFailure: error => error.Type == ErrorType.CircuitBreakerOpen
? "Service temporarily unavailable - circuit breaker is open"
: $"Error: {error.Message}"
);
The circuit breaker implements a 3-state model and only counts infrastructure errors (Unavailable, Timeout, Database, Unexpected) towards failure thresholds. Business errors (Validation, NotFound, Permission, etc.) are ignored.
ErrorType.CircuitBreakerOpenopenTimeout → transitions to HalfOpenhalfOpenMaxAttempts)var policy = new CircuitBreakerPolicy(
failureThreshold: 10, // Number of failures before opening
openTimeout: TimeSpan.FromMinutes(1), // How long to stay open
halfOpenMaxAttempts: 5 // Test attempts in half-open state
);
Best Practices:
Infrastructure Errors (counted towards threshold):
ErrorType.Unavailable - Service down, network issuesErrorType.Timeout - Request exceeded time limitErrorType.Database - Database connection/query failedErrorType.Unexpected - Unhandled exceptionsBusiness Errors (ignored by circuit breaker):
ErrorType.Validation - Invalid inputErrorType.NotFound - Resource doesn't existErrorType.Business - Business rule violationErrorType.Permission / ErrorType.Unauthorized - Access deniedErrorType.Conflict - Duplicate/collisionvar policy = new CircuitBreakerPolicy(
failureThreshold: 5,
openTimeout: TimeSpan.FromSeconds(30),
halfOpenMaxAttempts: 3
);
// Sync function
var result = await GetUserId()
.BindWithCircuitBreakerAsync(
id => _externalService.GetUserData(id),
policy
);
// Async function
var result = await GetUserIdAsync()
.BindWithCircuitBreakerAsync(
id => _externalService.GetUserDataAsync(id),
policy
);
var result = await ValidateRequestAsync(request)
.BindAsync(req => AuthenticateAsync(req))
.BindWithCircuitBreakerAsync(
user => _externalApi.FetchDataAsync(user),
circuitBreaker
)
.MapAsync(data => ProcessData(data));
// Check current state
switch (policy.State)
{
case CircuitBreakerState.Closed:
_logger.LogInfo("Circuit healthy");
break;
case CircuitBreakerState.Open:
_logger.LogWarning("Circuit open - service degraded");
break;
case CircuitBreakerState.HalfOpen:
_logger.LogInfo("Circuit testing recovery");
break;
}
// Manual reset if needed (e.g., after manual intervention)
policy.Reset();
var result = await operation.BindWithCircuitBreakerAsync(CallServiceAsync, policy);
result.Switch(
onSuccess: data => Console.WriteLine($"Success: {data}"),
onFailure: error =>
{
if (error.Type == ErrorType.CircuitBreakerOpen)
{
// Circuit breaker is open
var cbError = error; // Contains last failure in message
_logger.LogWarning($"Circuit open: {error.Message}");
// Implement fallback behavior
return GetCachedData();
}
else
{
// Other error types
_logger.LogError($"Operation failed: {error.Message}");
}
}
);
using Voyager.Common.Results.Extensions;
// Retry for transient failures + Circuit Breaker for cascading failures
var result = await GetConnectionAsync()
.BindWithRetryAsync(
conn => ExecuteQueryAsync(conn),
RetryPolicies.TransientErrors(maxAttempts: 3)
)
.BindWithCircuitBreakerAsync(
data => CallDownstreamServiceAsync(data),
circuitBreaker
);
The Resilience library is designed as a separate package to:
See ADR-0004 for architectural rationale.
Contributions are welcome! Please see CONTRIBUTING.md.
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for version history and release notes.