Skip to content

Best Practices

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

Best Practices

Production-ready guidelines for using Murphy effectively and safely.

General Principles

1. Start Simple, Add Complexity Gradually

// Start with just retry
Policy := TRetryBuilder.Handle(Exception).Retry(3).Build;

// Add circuit breaker when needed
// Add fallback for critical paths
// Combine multiple patterns only when necessary

2. Use Specific Exception Types

// Good - Specific exceptions
Policy := TRetryBuilder
  .Handle([EIdHTTPProtocolException, EIdSocketError])
  .Retry(3)
  .Build;

// Bad - Catches everything, may hide bugs
Policy := TRetryBuilder
  .Handle(Exception)
  .Retry(3)
  .Build;

3. Configure Timeouts Appropriately

// Good - Reasonable timeout based on SLA
Policy := TRetryBuilder
  .Handle(Exception)
  .Retry(3)
  .Wait(TTimeSpan.FromSeconds(2))  // Based on service recovery time
  .Build;

// Bad - Too short (likely to fail)
Policy := TRetryBuilder.Handle(Exception).Retry(10).Wait(TTimeSpan.FromMilliseconds(10)).Build;

// Bad - Too long (wastes user time)
Policy := TRetryBuilder.Handle(Exception).Retry(5).Wait(TTimeSpan.FromMinutes(5)).Build;

Retry Pattern Best Practices

Use Exponential Backoff for Network Operations

Policy := TRetryBuilder
  .Handle(EIdHTTPProtocolException)
  .Retry(5)
  .Wait(TTimeSpan.FromSeconds(1))
  .When(function(Ctx: TRetryContext): Boolean
        begin
          Ctx.WaitDelay := TTimeSpan.FromMilliseconds(
            Ctx.WaitDelay.TotalMilliseconds * 2);
          Result := True;
        end)
  .Build;

Don't Retry Non-Idempotent Operations Without Careful Design

// Dangerous - May create duplicate records
Policy.Execute(procedure begin Database.Execute('INSERT INTO orders...'); end);

// Better - Use idempotency key or check before insert
Policy.Execute(
  procedure
  begin
    if not OrderExists(OrderId) then
      Database.Execute('INSERT INTO orders...');
  end);

Log Retry Attempts

Policy := TRetryBuilder
  .Handle(Exception)
  .Retry(3)
  .When(function(Ctx: TRetryContext): Boolean
        begin
          Log.Warning(Format('Retry attempt %d after %s',
            [Ctx.Attempts, Ctx.Exception.Message]));
          Result := True;
        end)
  .Build;

Circuit Breaker Best Practices

Configure Based on Service Characteristics

// Fast-recovering service (network glitch)
Policy := TCircuitBreakerBuilder
  .Handle(Exception)
  .Fail(5)
  .Within(TTimeSpan.FromSeconds(10))
  .Build;

// Slow-recovering service (deployment, restart)
Policy := TCircuitBreakerBuilder
  .Handle(Exception)
  .Fail(3)
  .Within(TTimeSpan.FromMinutes(5))
  .Build;

Monitor and Alert on Circuit State Changes

type
  TMonitoredCircuitBreaker = class
  private
    FPolicy: ICircuitBreakerPolicy;
    FLastState: TCircuitState;
    procedure CheckStateChange;
  public
    procedure Execute(AProc: TProc);
  end;

procedure TMonitoredCircuitBreaker.Execute(AProc: TProc);
begin
  FPolicy.Execute(AProc);
  CheckStateChange;
end;

procedure TMonitoredCircuitBreaker.CheckStateChange;
begin
  var CurrentState := FPolicy.CircuitState;
  if CurrentState <> FLastState then
  begin
    Log.Warning(Format('Circuit state changed: %s → %s',
      [GetEnumName(TypeInfo(TCircuitState), Ord(FLastState)),
       GetEnumName(TypeInfo(TCircuitState), Ord(CurrentState))]));

    if CurrentState = TCircuitState.Open then
      AlertOps('Service circuit breaker opened');

    FLastState := CurrentState;
  end;
end;

Always Handle EBrokenCircuitException

try
  CircuitPolicy.Execute(procedure begin CallService; end);
except
  on E: EBrokenCircuitException do
  begin
    Log.Error('Service unavailable - circuit is open');
    // Provide fallback or user-friendly error
  end;
end;

Fallback Best Practices

Ensure Fallback is Reliable

// Good - Fallback doesn't throw exceptions
Policy := TFallbackBuilder<string>
  .Handle(Exception)
  .Fallback(function: string
            begin
              try
                Result := GetCachedData;
              except
                Result := 'Default Value';  // Ultimate fallback
              end;
            end)
  .Build;

Indicate Degraded Mode to Users

var Data := FallbackPolicy.Execute(
  function: string
  begin
    try
      Result := FetchLiveData;
    except
      ShowWarning('Using cached data - live data unavailable');
      raise;
    end;
  end);

Cache Management

type
  TCachedService = class
  private
    FCache: string;
    FCacheTime: TDateTime;
  public
    function GetData: string;
  end;

function TCachedService.GetData: string;
var
  Policy: IFallbackPolicy<string>;
begin
  Policy := TFallbackBuilder<string>
    .Handle(Exception)
    .Fallback(function: string
              begin
                var Age := SecondsBetween(Now, FCacheTime);
                if Age > 3600 then
                  Log.Warning(Format('Cache is %d seconds old', [Age]));
                Result := FCache;
              end)
    .Build;

  Result := Policy.Execute(
    function: string
    begin
      Result := FetchFreshData;
      FCache := Result;
      FCacheTime := Now;
    end);
end;

Rate Limit Best Practices

Add Safety Margin

// If API allows 100 req/min, configure for 95 to add safety margin
Policy := TRateLimitBuilder
  .Handle(Exception)
  .Allow(95)
  .Within(TTimeSpan.FromMinutes(1))
  .Build;

Handle Rate Limit Rejections Gracefully

try
  RateLimitPolicy.Execute(procedure begin MakeAPICall; end);
except
  on E: ERateLimitRejectedException do
  begin
    // Queue for later
    QueueForRetry(Request);
    // Or inform user
    ShowMessage('Request queued - rate limit reached');
  end;
end;

Use Separate Policies for Different Resources

type
  TAPIClient = class
  private
    FSearchRateLimit: IRateLimitPolicy;
    FUpdateRateLimit: IRateLimitPolicy;
  public
    constructor Create;
  end;

constructor TAPIClient.Create;
begin
  // Different limits for different endpoints
  FSearchRateLimit := TRateLimitBuilder.Handle(Exception)
    .Allow(100).Within(TTimeSpan.FromMinutes(1)).Build;

  FUpdateRateLimit := TRateLimitBuilder.Handle(Exception)
    .Allow(10).Within(TTimeSpan.FromMinutes(1)).Build;
end;

Combining Patterns Best Practices

Order Matters - Choose Wisely

// Recommended: Circuit outside Retry
CircuitBreaker.Execute(procedure
  begin
    Retry.Execute(procedure begin CallService; end);
  end);

// Circuit tracks retry failures
// Opens faster on persistent issues

Test Pattern Combinations

procedure TestCombinedPolicies;
var
  Retry: IRetryPolicy;
  Fallback: IFallbackPolicy<string>;
begin
  MurphyTestModeEnabled := True;

  Retry := TRetryBuilder.Handle(Exception).Retry(2).Build;
  Fallback := TFallbackBuilder<string>.Handle(Exception)
    .Fallback(function: string begin Result := 'Fallback'; end).Build;

  // Test that retry happens before fallback
  var Attempts := 0;
  var Result := Fallback.Execute(
    function: string
    begin
      Retry.Execute(
        procedure
        begin
          Inc(Attempts);
          raise Exception.Create('Test');
        end);
    end);

  Assert.AreEqual(3, Attempts);  // Verify retry happened
  Assert.AreEqual('Fallback', Result);  // Verify fallback activated

  MurphyTestModeEnabled := False;
end;

Performance Considerations

Reuse Policy Instances

// Good - Create once, use many times
type
  TService = class
  private
    FRetryPolicy: IRetryPolicy;
  public
    constructor Create;
    procedure Operation1;
    procedure Operation2;
  end;

constructor TService.Create;
begin
  FRetryPolicy := TRetryBuilder.Handle(Exception).Retry(3).Build;
end;

procedure TService.Operation1;
begin
  FRetryPolicy.Execute(procedure begin ... end);
end;

// Bad - Create new policy each time
procedure BadExample;
begin
  TRetryBuilder.Handle(Exception).Retry(3).Build
    .Execute(procedure begin ... end);
end;

Don't Overuse Policies

// Bad - Unnecessary for local, reliable operations
Policy.Execute(procedure begin LocalVariable := 1; end);

// Good - Use only for operations that can actually fail
Policy.Execute(procedure begin HTTP.Get(URL); end);

Monitoring and Observability

Add Metrics

type
  TMetricsWrapper = class
  private
    FPolicy: IRetryPolicy;
  public
    RetryCount: Integer;
    SuccessAfterRetry: Integer;
    procedure Execute(AProc: TProc);
  end;

procedure TMetricsWrapper.Execute(AProc: TProc);
var
  Attempts: Integer = 0;
begin
  FPolicy.When(
    function(Ctx: TRetryContext): Boolean
    begin
      Inc(FRetryCount);
      Result := True;
    end)
    .Execute(AProc);

  if Attempts > 1 then
    Inc(SuccessAfterRetry);
end;

Log Important Events

// Circuit state changes
// Retry exhaustion
// Fallback activation
// Rate limit rejections

Security Considerations

Don't Log Sensitive Data

// Bad - May log passwords, tokens
Policy.When(function(Ctx: TRetryContext): Boolean
  begin
    Log.Debug('Exception: ' + Ctx.Exception.ToString);  // May contain secrets
  end);

// Good - Log safely
Policy.When(function(Ctx: TRetryContext): Boolean
  begin
    Log.Debug(Format('Retry attempt %d: %s', [Ctx.Attempts, Ctx.Exception.ClassName]));
  end);

Validate Before Retry

// Ensure retry won't cause security issues
Policy.Execute(
  procedure
  begin
    if not ValidateRequest(Request) then
      raise ESecurityException.Create('Invalid request');
    ProcessRequest(Request);
  end);

Back to Index

Clone this wiki locally