DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Claude Codeでリクエストコアレッシングを設計する:重複クエリ排除・キャッシュスタンピード対策

はじめに

「同じAPIに同時に100リクエストが来てDBに100回同じクエリが走る」——リクエストコアレッシング(Request Coalescing)で同一クエリを1回にまとめ、結果を全リクエスターに返す設計をClaude Codeに生成させる。


CLAUDE.mdに設計ルールを書く

## リクエストコアレッシング設計ルール

### 適用条件
- 同一キーに対して複数の同時リクエストが予想されるエンドポイント
- キャッシュミス時のDB/外部API呼び出しが重い処理
- 副作用なしで共有できる読み取りクエリ

### 実装方式
- インメモリMap + Promise: 同一プロセス内の重複排除
- Redis Lock + Pub/Sub: 複数サーバーインスタンス間の重複排除
- DataLoader: GraphQL/バッチ読み取り向けN+1排除
Enter fullscreen mode Exit fullscreen mode

生成される実装(抜粋)

// インプロセスコアレッシング
export class InProcessCoalescer<T> {
  private readonly inflight = new Map<string, Promise<T>>();

  async dedupe(key: string, fn: () => Promise<T>): Promise<T> {
    const existing = this.inflight.get(key);
    if (existing) {
      metrics.counter('coalescing_saved_calls', 1, { key });
      return existing; // 進行中のPromiseを共有
    }

    const promise = fn();
    this.inflight.set(key, promise);
    try {
      return await promise;
    } finally {
      this.inflight.delete(key); // 完了後に削除
    }
  }
}

// 分散コアレッシング(Redis)
export class DistributedCoalescer<T> {
  async dedupe(key: string, fn: () => Promise<T>): Promise<T> {
    return this.localCoalescer.dedupe(key, async () => {
      const cached = await redis.get(resultKey);
      if (cached) return JSON.parse(cached);

      const lockAcquired = await redis.set(lockKey, '1', { NX: true, PX: 10_000 });
      if (lockAcquired) {
        const result = await fn();
        await redis.set(resultKey, JSON.stringify(result), { PX: 30_000 });
        await redis.publish(`${lockKey}:done`, '1'); // ウェイターに通知
        return result;
      } else {
        return this.waitForResult(resultKey, lockKey); // 完了を待機
      }
    });
  }
}

// BatchCoalescer(DataLoader方式)
const userBatchLoader = new BatchCoalescer<string, User>(async (ids) => {
  const users = await prisma.user.findMany({ where: { id: { in: ids } } });
  return new Map(users.map(u => [u.id, u]));
}, 1 /* 1msバッチウィンドウ */);

// N+1が自動的にバッチ化される
const resolvers = { Order: { user: (order) => userBatchLoader.load(order.userId) } };
Enter fullscreen mode Exit fullscreen mode

まとめ

  1. CLAUDE.md に適用条件(読み取り・副作用なし)・インプロセス/分散の使い分け・DataLoaderによるN+1排除を明記
  2. InProcessCoalescer でMap+Promiseの共有——同一プロセス内なら追加依存なしで実装可能
  3. DistributedCoalescer でRedis SET NX + Pub/Sub——マルチサーバー環境でも1台だけ計算し結果を全サーバーに配布
  4. BatchCoalescer(DataLoader方式) で1msウィンドウ内の複数loadを1つのIN句クエリに——GraphQLのN+1問題を透過的に解決

パフォーマンス設計のレビューは **Code Review Pack(¥980)* の /code-review で確認できます。*

prompt-works.jp

みょうが (@myougatheaxo) — ウーパールーパーのVTuber。

Top comments (0)