-
Notifications
You must be signed in to change notification settings - Fork 1
Combining Patterns
Murphy's true power comes from combining multiple resilience patterns together.
Retry failed operations, then fall back to cached data if retries are exhausted.
function GetUserData(UserId: Integer): string;
var
RetryPolicy: IRetryPolicy;
FallbackPolicy: IFallbackPolicy<string>;
begin
RetryPolicy := TRetryBuilder
.Handle(EIdHTTPProtocolException)
.Retry(3)
.Wait(TTimeSpan.FromSeconds(1))
.Build;
FallbackPolicy := TFallbackBuilder<string>
.Handle(Exception)
.Fallback(function: string begin Result := GetCachedUserData(UserId); end)
.Build;
Result := FallbackPolicy.Execute(
function: string
begin
RetryPolicy.Execute(
procedure
begin
Result := HTTP.Get(Format('/users/%d', [UserId]));
end);
end);
end;Fast-fail when service is down, provide fallback data immediately.
function FetchData: string;
var
CircuitPolicy: ICircuitBreakerPolicy;
FallbackPolicy: IFallbackPolicy<string>;
begin
CircuitPolicy := TCircuitBreakerBuilder
.Handle(Exception)
.Fail(5)
.Within(TTimeSpan.FromSeconds(30))
.Build;
FallbackPolicy := TFallbackBuilder<string>
.Handle([Exception, EBrokenCircuitException])
.Fallback(function: string
begin
WriteLn('Using fallback data');
Result := CachedData;
end)
.Build;
Result := FallbackPolicy.Execute(
function: string
begin
CircuitPolicy.Execute(procedure begin Result := FetchFromAPI; end);
end);
end;Retry transient failures, but open circuit for persistent failures.
var
Retry: IRetryPolicy;
Circuit: ICircuitBreakerPolicy;
begin
Retry := TRetryBuilder.Handle(Exception).Retry(2).Build;
Circuit := TCircuitBreakerBuilder.Handle(Exception).Fail(5).Build;
// Circuit wraps retry - tracks retry exhaustion as failures
Circuit.Execute(
procedure
begin
Retry.Execute(procedure begin CallService; end);
end);
end;Retry when rate limit is exceeded after a delay.
var
RateLimit: IRateLimitPolicy;
Retry: IRetryPolicy;
begin
RateLimit := TRateLimitBuilder.Handle(Exception)
.Allow(100).Within(TTimeSpan.FromMinutes(1)).Build;
Retry := TRetryBuilder
.Handle(ERateLimitRejectedException)
.Retry(3)
.Wait(TTimeSpan.FromSeconds(20))
.Build;
Retry.Execute(
procedure
begin
RateLimit.Execute(procedure begin MakeAPICall; end);
end);
end;Maximum resilience for critical operations.
function GetCriticalData: string;
var
Retry: IRetryPolicy;
Circuit: ICircuitBreakerPolicy;
Fallback: IFallbackPolicy<string>;
begin
Retry := TRetryBuilder
.Handle(EIdHTTPProtocolException)
.Retry(2)
.Wait(TTimeSpan.FromSeconds(1))
.Build;
Circuit := TCircuitBreakerBuilder
.Handle(Exception)
.Fail(5)
.Within(TTimeSpan.FromMinutes(1))
.Build;
Fallback := TFallbackBuilder<string>
.Handle([Exception, EBrokenCircuitException])
.Fallback(function: string begin Result := GetFromCache; end)
.Build;
// Fallback → Circuit → Retry → Actual Operation
Result := Fallback.Execute(
function: string
begin
Circuit.Execute(
procedure
begin
Retry.Execute(
procedure
begin
Result := FetchFromAPI;
end);
end);
end);
end;Every example so far combines policies by manually nesting Execute calls. The Policy Wrap pattern does the same thing without the growing indentation: Wrap([A, B]) executes A(B(action)), exactly like calling A.Execute with a closure that calls B.Execute.
The manual Retry + Timeout combination:
Retry.Execute(
procedure
begin
Timeout.Execute(procedure begin CallExternalService; end);
end);is equivalent to:
uses Murphy.Policy.Wrap;
var Policy: IPolicyWrap;
begin
Policy := TPolicyWrapBuilder.Wrap([Retry, Timeout]);
Policy.Execute(procedure begin CallExternalService; end);
end;Policy Wrap only accepts IExecutablePolicy implementations - Retry, Circuit Breaker, Rate Limit, Timeout, Bulkhead, Hedging, and other wraps. IFallbackPolicy<TResult> and ICachePolicy<TResult> are not IExecutablePolicy (they return a value via TFunc<TResult>), so a Fallback or Cache policy at the edge of a composition still needs to wrap the IPolicyWrap.Execute call manually:
uses Murphy.Policy.Fallback, Murphy.Policy.Retry, Murphy.Policy.CircuitBreaker, Murphy.Policy.Wrap;
function GetCriticalData: string;
var
Resilient: IPolicyWrap;
Fallback: IFallbackPolicy<string>;
begin
Resilient := TPolicyWrapBuilder.Wrap([
TRetryBuilder.Handle(EIdHTTPProtocolException).Retry(2).Wait(TTimeSpan.FromSeconds(1)),
TCircuitBreakerBuilder.Handle(Exception).Fail(5).Within(TTimeSpan.FromMinutes(1))
]);
Fallback := TFallbackBuilder<string>
.Handle([Exception, EBrokenCircuitException])
.Fallback(function: string begin Result := GetFromCache; end)
.Build;
Result := Fallback.Execute(
function: string
begin
Resilient.Execute(
procedure
begin
Result := FetchFromAPI;
end);
end);
end;See the Policy Wrap Pattern Guide and its API Reference for more details.
The order in which you combine policies affects behavior:
Circuit.Execute(procedure
begin
Retry.Execute(procedure begin CallService; end);
end);- Circuit tracks retry exhaustion as failures
- Opens after multiple retry failures
- More responsive to persistent issues
Retry.Execute(procedure
begin
Circuit.Execute(procedure begin CallService; end);
end);- Retries even after circuit opens
- May waste retries on open circuit
- Less efficient
- Start simple - Add patterns incrementally
- Test combinations - Ensure they interact correctly
- Consider order - Think about execution flow
- Log interactions - Track which policy triggered
- Monitor metrics - Understand actual behavior
type
TResilientAPIClient = class
private
FRetry: IRetryPolicy;
FCircuit: ICircuitBreakerPolicy;
FRateLimit: IRateLimitPolicy;
FFallback: IFallbackPolicy<TJSONObject>;
public
constructor Create;
function GetUser(UserId: Integer): TJSONObject;
end;
constructor TResilientAPIClient.Create;
begin
FRateLimit := TRateLimitBuilder.Handle(Exception)
.Allow(100).Within(TTimeSpan.FromMinutes(1)).Build;
FRetry := TRetryBuilder.Handle(EIdHTTPProtocolException)
.Retry(3).Wait(TTimeSpan.FromSeconds(1)).Build;
FCircuit := TCircuitBreakerBuilder.Handle(Exception)
.Fail(10).Within(TTimeSpan.FromMinutes(5)).Build;
FFallback := TFallbackBuilder<TJSONObject>
.Handle([Exception, EBrokenCircuitException, ERateLimitRejectedException])
.Fallback(function: TJSONObject begin Result := GetCachedUser; end)
.Build;
end;
function TResilientAPIClient.GetUser(UserId: Integer): TJSONObject;
begin
// Fallback → Circuit → Retry → RateLimit → API
Result := FFallback.Execute(
function: TJSONObject
begin
FCircuit.Execute(
procedure
begin
FRetry.Execute(
procedure
begin
FRateLimit.Execute(
procedure
begin
Result := FetchUserFromAPI(UserId);
end);
end);
end);
end);
end;