Skip to content

Combining Patterns

Marco Breveglieri edited this page Jul 20, 2026 · 2 revisions

Combining Patterns

Murphy's true power comes from combining multiple resilience patterns together.

Common Combinations

Retry + Fallback

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;

Circuit Breaker + Fallback

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 + Circuit Breaker

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;

Rate Limit + Retry

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;

Complete Stack: Retry + Circuit Breaker + Fallback

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;

Policy Wrap: A Cleaner Way to Combine Policies

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.

Ordering Matters

The order in which you combine policies affects behavior:

Circuit Outside Retry (Recommended)

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 Outside Circuit

Retry.Execute(procedure
  begin
    Circuit.Execute(procedure begin CallService; end);
  end);
  • Retries even after circuit opens
  • May waste retries on open circuit
  • Less efficient

Best Practices

  1. Start simple - Add patterns incrementally
  2. Test combinations - Ensure they interact correctly
  3. Consider order - Think about execution flow
  4. Log interactions - Track which policy triggered
  5. Monitor metrics - Understand actual behavior

Real-World Example

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;

Back to Index

Clone this wiki locally