-
Notifications
You must be signed in to change notification settings - Fork 1
Best Practices
Marco Breveglieri edited this page Jul 20, 2026
·
2 revisions
Production-ready guidelines for using Murphy effectively and safely.
// 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// Good - Specific exceptions
Policy := TRetryBuilder
.Handle([EIdHTTPProtocolException, EIdSocketError])
.Retry(3)
.Build;
// Bad - Catches everything, may hide bugs
Policy := TRetryBuilder
.Handle(Exception)
.Retry(3)
.Build;// 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;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;// 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);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;// 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;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;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;// 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;var Data := FallbackPolicy.Execute(
function: string
begin
try
Result := FetchLiveData;
except
ShowWarning('Using cached data - live data unavailable');
raise;
end;
end);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;// If API allows 100 req/min, configure for 95 to add safety margin
Policy := TRateLimitBuilder
.Handle(Exception)
.Allow(95)
.Within(TTimeSpan.FromMinutes(1))
.Build;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;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;// Recommended: Circuit outside Retry
CircuitBreaker.Execute(procedure
begin
Retry.Execute(procedure begin CallService; end);
end);
// Circuit tracks retry failures
// Opens faster on persistent issuesprocedure 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;// 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;// 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);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;// Circuit state changes
// Retry exhaustion
// Fallback activation
// Rate limit rejections// 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);// Ensure retry won't cause security issues
Policy.Execute(
procedure
begin
if not ValidateRequest(Request) then
raise ESecurityException.Create('Invalid request');
ProcessRequest(Request);
end);