キャッシュとリモートフェッチを組み合わせるとき、ロジックが混在しがちだ。オーケストレーション関数としてまとめることで、キャッシュヒット・ミスの両パスを整理し、メトリクス計測や副作用の委譲も一箇所で管理できる。
型定義
interface CacheProvider<T> { get(key: string): Promise<T | undefined>; set(key: string, value: T): Promise<void>;}
interface RemoteProvider<T> { fetch(key: string): Promise<T>;}
interface MetricsReporter { recordCacheHit(key: string, latencyMs: number): void; recordCacheMiss(key: string): void; recordFetchLatency(key: string, latencyMs: number): void; recordOutcome(key: string, outcome: 'success' | 'error', latencyMs: number): void;}オーケストレーション関数
async function orchestrate<T>( key: string, cache: CacheProvider<T>, remote: RemoteProvider<T>, metrics: MetricsReporter,): Promise<T> { const start = performance.now();
// 高速パス:キャッシュヒット const cached = await cache.get(key); if (cached !== undefined) { metrics.recordCacheHit(key, performance.now() - start); return cached; }
// 低速パス:ライブフェッチ metrics.recordCacheMiss(key); const fetchStart = performance.now(); try { const data = await remote.fetch(key); metrics.recordFetchLatency(key, performance.now() - fetchStart);
// 副作用は外部に委譲(fire and forget) void cache.set(key, data); metrics.recordOutcome(key, 'success', performance.now() - start); return data; } catch (error) { metrics.recordOutcome(key, 'error', performance.now() - start); throw error; }}使用例
const result = await orchestrate( 'user:42', redisCache, userApiClient, datadogMetrics,);解説
- 高速パス:キャッシュにヒットすれば即リターン。リモートは呼ばない。
- 低速パス:キャッシュミス時のみリモートへフォールバック。取得後にキャッシュを更新する(fire and forget)。
- メトリクス:キャッシュヒット・ミス・フェッチのレイテンシ・最終結果をすべて計測。
MetricsReporterインタフェースに委譲するため、Datadog / Prometheus / ログなど実装に依存しない。 - 副作用の委譲:キャッシュ書き込みやメトリクス送信はオーケストレーション関数の外に切り出されている。テストが容易で、実装を差し替えやすい。
応用
このパターンは以下のような処理に適している。
- インポーター:外部データソースから取り込む際、済み分を高速スキップ
- データエンリッチメントジョブ:補完済みエンティティを再処理しない
- 同期ハンドラー:重複取得を避けながらメトリクスで差分を追跡
- コストの高いUIアクション:初回フェッチのみ実行し、以降はキャッシュで応答
CacheProvider / RemoteProvider / MetricsReporter はそれぞれインタフェースとして分離されているため、Redis/in-memory、REST/gRPC、Datadog/StatsD など任意の実装を組み合わせられる。
実務メモ
このスニペットは、TypeScript、design-pattern、performance、observability の周辺で同じ操作や判定を毎回書きたくない時に向く。小さな補助として切り出しておくと、呼び出し側では意図だけを追いやすい。
逆に、分岐や前提条件が増えて責務が膨らむなら、1本のスニペットに詰め込まない方がよい。手順と helper を分けるか、役割ごとに切り出す方が保守しやすい。
hsb.horse