Skip to content
Marco Breveglieri edited this page Jul 20, 2026 · 1 revision

Cache Pattern Guide

The Cache pattern caches the result of an expensive operation for a configurable duration, reducing load on backends and serving a stale-but-known-good value when a refresh fails.

When to Use

  • Expensive lookups (remote configuration, reference data) that don't need to be up-to-the-second
  • Reducing load on a backend that would otherwise be called on every request
  • Degrading gracefully to the last known good value when a refresh fails

When NOT to Use

  • Data that must always be fresh (e.g. account balances, one-time tokens)
  • Caching mutable class instances the policy would need to own - prefer interfaces or records for TResult
  • As a substitute for a proper distributed cache when multiple processes must share the same cached value

Quick Start

uses Murphy.Policy.Cache;

var
  Policy: ICachePolicy<string>;
  Value: string;
begin
  Policy := TCacheBuilder<string>
    .Handle([])
    .Expire(TTimeSpan.FromMinutes(5))
    .Build;

  Value := Policy.Execute(function: string begin Result := FetchExpensiveData; end);
end;

Common Scenarios

Serving Stale Data on Failure

function GetRemoteConfig: string;
var
  Policy: ICachePolicy<string>;
begin
  Policy := TCacheBuilder<string>
    .Handle(ENetworkError) // Serve the last cached value on this failure
    .Expire(TTimeSpan.FromMinutes(5))
    .Build;

  Result := Policy.Execute(
    function: string
    begin
      Result := FetchConfigFromServer;
    end);
end;

Manually Invalidating the Cache

var Policy: ICachePolicy<string>;
begin
  Policy := TCacheBuilder<string>.Handle([]).Expire(TTimeSpan.FromMinutes(10)).Build;

  // Force the next call to refresh, regardless of expiry
  Policy.Invalidate;
end;

Caching a Record

type
  TUserProfile = record
    Name: string;
    Email: string;
  end;

var
  Policy: ICachePolicy<TUserProfile>;
  Profile: TUserProfile;
begin
  Policy := TCacheBuilder<TUserProfile>
    .Handle(Exception)
    .Expire(TTimeSpan.FromMinutes(2))
    .Build;

  Profile := Policy.Execute(
    function: TUserProfile
    begin
      Result := FetchUserProfile;
    end);
end;

One Policy per Cached Value

type
  TCacheService = class
  private
    FUserCache: ICachePolicy<string>;
    FSettingsCache: ICachePolicy<string>;
  public
    constructor Create;
  end;

constructor TCacheService.Create;
begin
  // Each cached operation needs its own policy instance
  FUserCache := TCacheBuilder<string>.Handle(Exception).Expire(TTimeSpan.FromMinutes(5)).Build;
  FSettingsCache := TCacheBuilder<string>.Handle(Exception).Expire(TTimeSpan.FromHours(1)).Build;
end;

Best Practices

  1. One instance per operation - a ICachePolicy<TResult> caches a single value; don't share one instance across unrelated calls
  2. Filter refresh failures with Handle() - only the exception types passed there fall back to the stale value; anything else re-raises
  3. Prefer interfaces or records for TResult - class instances are not owned or freed by the policy
  4. Choose expiry based on staleness tolerance - not on how expensive the operation is
  5. Use Invalidate after a known write - force a fresh read right after data changes upstream

See Also


Back to Index

Clone this wiki locally