composition-root について
Cloudflare アプリケーションの構成ルートとライフサイクル単位の依存関係を整理するコーディングスキルです。アプリケーション所有のポートでプラットフォームバインディングを隔離し、各ランタイムのエントリーポイントで境界を検証します。
主な機能
- 01エントリーポイント、バインディング、ライフサイクルの整理
- 02アプリケーション所有のポートと依存関係の定義
- 03バインディングアダプターとアプリケーションポリシーの分離
- 04ランタイム面ごとの依存関係の構成
- 05テストと検索による境界の検証
活用シーン
- 01Cloudflare バインディングを直接使うサービスのリファクタリング
- 02Workerやスケジュール実行の入口にサービスを追加
- 03アプリケーションモジュールからランタイム型を排除
- 04トレーシングと副作用の所有範囲を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ alexander-zuev/ agent-skills --skill composition-rootインストール後、Claude Code で/composition-rootを入力して読み込まれたか確認します。
同梱ファイルEXAMPLES.md
詳細
Cloudflare のコンポジションルート
ランタイム機能の依存関係は、それを所有するライフサイクルで組み立てるために、コンポジションルートを使用します。
構築コードや呼び出しコードを配置する前に、システムのエントリーポイントの特定と依存関係の初期化を適用します。
内部コードに渡すのはポートだけです。Env、生のバインディング、フレームワークのサービスロケーター、バインディング名は渡しません。
Cloudflare entrypoint
→ tracer + parsed configuration
→ binding adapters
→ exact service dependencies
→ application service
→ protocol projection
TypeScript の契約、解析、エラー、副作用、テストには ../typescript-standards/SKILL.md を適用します。実装の形が明確でない場合は、TanStack Start、WorkerEntrypoint、アダプター、リファクタリングのテンプレートを EXAMPLES.md で確認します。
所有権
- コンポジションルート: 所有するスコープに応じてアダプターとサービスを構築し、運用エントリーポイントに渡します。
- エントリーポイントアダプター: 外部入力を解析し、呼び出しコンテキストを確立し、渡された依存関係を使ってサービスを呼び出し、その結果を出力形式に変換します。
- バインディングアダプター: KV、R2、D1、Durable Objects、Queues、サービスバインディング、Workflows、その他の外部機能を使用して、アプリケーション所有のポートを実装します。
- アプリケーションサービス: 1 つのユースケースにおける処理の順序、フォールバック、再試行、ベストエフォートの方針を所有します。
- ドメインモジュール: 純粋な値と不変条件を所有します。
- ポート: 利用側が必要とする機能を正確に定義します。Cloudflare API をそのまま反映するものではありません。
プラットフォームの生の値を扱えるのは、コンポジションルートとバインディングアダプターだけです。バインディングアダプターが受け取るのは、Env 全体ではなく、必要最小限のプラットフォーム機能です。
1. 境界を把握する
リポジトリの指示と、影響を受ける以下の項目を調べます。
- TanStack Start の
src/server.ts、リクエストコンテキストの型、ミドルウェアの順序、サーバー関数。 - Worker、WorkerEntrypoint、Durable Object、Workflow、キュー、スケジュール済みエントリーポイント。
Envの宣言と、関連するバインディングのすべての使用箇所。- トレーサーの作成、相関付け、エラー報告、切り離された作業の所有権。
- 既存のサービス、アダプター、ポート、テスト、アーキテクチャチェック。
影響を受ける動作を、エントリーポイントから副作用、呼び出し元に見える結果までたどります。リファクタリングでは、移動する前に現在のエラー、再試行、フォールバック、シリアライズ、トレースの動作を記録します。
完了基準: 影響を受けるすべてのエントリーポイント、バインディングの利用側、動作、ライフサイクル、副作用、障害方針、既存のテストシームが把握されています。
2. アプリケーション所有の依存関係を設計する
利用側のニーズに基づいて契約を定義します。
/** Persistence capability required by document application services. */
export interface DocumentStore {
/** Find one document by its domain ID. */
find(id: DocumentId): Promise<Document | null>;
/** Persist one valid document. */
save(document: Document): Promise<void>;
}
/** Exact dependencies of the publish-document use case. */
export type PublishDocumentDependencies = Readonly<{
documents: DocumentStore;
clock: Clock;
tracer: Tracer;
}>;
ルール:
- 生のキー、文字列、リクエストオブジェクト、バインディングオプションではなく、ドメイン入力と解析済み出力を使用します。
- 実際の呼び出し元が必要とする操作だけを公開します。
- 各サービスの依存関係オブジェクトを厳密に保ちます。共有の巨大な依存関係オブジェクトを作成したり、
EnvをDependenciesに名前変更したりしてはいけません。 - プラットフォームのシリアライズ、キー空間、ページネーショントークン、TTL、メタデータ、出力の解析は、バインディングアダプターに配置します。
- ポリシーはアプリケーションサービスに持たせます。アダプターは、サービスが復旧可能な条件と破損や不具合を区別できるよう、障害を正確に変換します。
- スパンを作成するサービスとアダプターには
Tracerを注入します。すべてのメソッドシグネチャに追加してはいけません。 - スコープがコンポジション後に実際に動的となる場合(認証後にアカウントに紐づく構築を行う場合など)に限り、アプリケーション所有のファクトリーを追加します。ファクトリーで
Envや生のバインディングを公開してはいけません。
ライフタイムを明示的に選択します。
- バインディングとトレーサーが並行呼び出し間で安全に使用できる場合は、不変のアダプターとサービスを再利用します。
- リクエストスコープのランタイムオブジェクトや可変の呼び出し状態を保持する場合は、呼び出しごとに構築します。
- 切り離された作業は、範囲を絞ったバックグラウンドタスク用ポートなど、明示的な所有者に渡します。
ExecutionContextを内側に漏らしてはいけません。
完了基準: 内部のすべてのシグネチャでアプリケーション/ドメイン型を使用し、各操作とファクトリーに現在の呼び出し元があり、依存関係、トレーサー、副作用のライフタイムに明示的な所有者がいます。
3. 構築またはリファクタリングする
新しいサービスブランチ
- TanStack や Cloudflare のランタイム型をインポートせず、ポートに対してアプリケーションサービスを実装します。
- 外部の役割ごとにバインディングアダプターを1つ実装します。
WorkersKvDocumentStoreのように、テクノロジー固有の実装には明示的な名前を付けます。 - プロトコルに関する処理は、HTTP、RPC、キュー、Workflow、またはスケジュールアダプターに置きます。
- ユースケースを提供するすべての運用エントリーポイントに、組み立てたサービスを渡します。
既存コードブランチ
- 根拠が不足している場合は、現在の公開境界でテストを行い、呼び出し側から見える動作を維持します。
- 既存の操作に基づき、直接の利用側にポートを定義します。
- ポリシーを変更する前に、既存の生バインディングをアダプターの背後に置きます。
- 直接の利用側が依存するものを、生バインディングからポートに置き換えます。
- アダプターの構築を、一度に1層ずつ外側へ移します。中間層が渡すのは型付きポートのみとし、ポートと生バインディングの両方を恒久的な設計として渡してはなりません。
- 混在したモジュールを分割します。ポリシーはアプリケーションサービスへ、プラットフォームの仕組みはアダプターへ、プロトコルのマッピングはエントリーポイントに残します。
- 最後の呼び出し側を移行したら、不要になった
Envパラメーター、ファクトリー、汎用ラッパー、重複するパーサーやキー生成処理、互換性のためのオーバーロードを削除します。
対称性だけを理由に、無関係なバインディングまでリファクタリングを広げないでください。
完了条件: 新しいコードが、提供するすべてのサーフェスで完全に組み立てられているか、移行したすべての生バインディングがアダプターにしか到達しないこと。動作やポリシーの変更は行わないか、明示的に要望され、テストされていること。
4. すべてのランタイムサーフェスを組み立てる
各コンポジションルートで、次の順に処理します。
- 設定とエントリーポイントのプロパティを解析します。
- トレーサーを作成するか、取得します。
- トレース対象の処理を呼び出す前に、相関関係を確立します。
- 必要に応じてトレーサーを注入しながら、生バインディングをアダプターでラップします。
- サービスに必要な依存関係だけを正確に組み立てます。
- 薄いプロトコルアダプターを介してサービスを呼び出します。
- 結果と安全なエラーを外部コントラクトにマッピングします。
TanStack Start の場合:
- 各 fetch、キュー、スケジュール呼び出しに対し、
src/server.tsでcreateAppDeps(env, ctx)のような単一のファクトリーを使って依存関係グラフを構築します。 - リクエストコンテキストとして Start ハンドラーに渡し、
Registerを通じて型を宣言します。 - サーバー関数とミドルウェアは、
context.depsから必要な機能だけを読み取ります。envやバインディング名を読み取ってはなりません。 - アプリ内の深い階層で無関係なトレーサーを作成するのではなく、ルートのトレーシングミドルウェアが使用するトレーサーを注入します。
コンポジションルートは、コンストラクターやハンドラーの名前ではなく、依存関係のライフタイムに基づいて選択します。 運用エントリーポイント間で依存関係ファクトリーを再利用します。各ルートや SDK コールバック内に別個のグラフを構築してはなりません。
完了条件: 提供するすべてのサーフェスに、そのスコープの依存関係グラフが渡されていること。内部モジュールがランタイムリソースを取得したり、バインディング名を読み取ったりしないこと。
5. 境界を検証する
本番の境界を通じて検証します。
- 実際のポートを実装した、小規模な記録用フェイクによるアプリケーションサービスの動作
- Cloudflare の代表的なローカルランタイムを用いた、バインディングアダプターのシリアライズ、解析、失敗時の動作
- エントリーポイントの組み立てと、外部向けの結果およびエラーへの変換
- スパン名、相関関係、安全な属性、切り離された作業の所有関係
- 型チェック、対象を絞ったテスト、アーキテクチャチェック、リポジトリで必須の検証
プロジェクトのバインディング名とランタイム型を使って、漏出を検索します。例:
rg 'Env|KVNamespace|R2Bucket|D1Database|DurableObjectNamespace|ExecutionContext' src
すべての検索結果を、コンポジションルート、バインディングアダプター、避けられないフレームワーク宣言、違反のいずれかに分類します。また、ルート、サービス、ドメイン、機能モジュールにおける context.env とバインディング名への直接アクセスも検索します。
完了条件: すべての生のプラットフォーム参照が分類され、許可されていること。変更したすべてのポート、アダプター、サービス、エントリーポイント、失敗経路、ライフサイクルに検証の根拠があること。また、アプリケーションモジュールが Cloudflare のランタイム型をインポートまたは受け取らないこと。