クリーンアーキテクチャにおける依存関係の集約 〜 Composition Root パターンの導入
Published on 2026年2月5日
アーキテクチャはじめに
本記事では、API層からインフラ層への依存を整理し、Composition Root パターンを導入した経緯と実装について解説します。
前提としているアーキテクチャ
本記事では「クリーンアーキテクチャ」をベースにした、以下のようなレイヤ構造を前提としています。
【内側】
Domain 層 ← Entity, Repository インターフェース
↑
UseCase / Application 層
↑
API(Interface)層 ← Hono の Handler など
↑
Infrastructure 層 ← DB接続, Repository 実装
【外側】
※ 矢印は依存の向き(外側 → 内側)
クリーンアーキテクチャでは「内側ほどビジネスルール、外側ほど詳細」という原則があり、依存は常に外側から内側に向かいます。Domain が最も内側にあり、API層や Infrastructure層は外側に位置します。
このレイヤ構造自体は、クリーンアーキテクチャの原則に沿った設計です。
しかし、問題が1つありました。
「どこで依存を組み立てるか」が明確でなく、その穴を API Handler が埋めてしまっていたのです。
// API Handler が Infrastructure の詳細を知っている
import { getDb } from "../infrastructure/database";
import { createUserRepository } from "../infrastructure/repositories/userRepository";
const userRepository = createUserRepository(getDb());
本記事では、この「依存の組み立て」を Composition Root(Container)に集約することで、レイヤ間の責務を明確にした過程を解説します。
選択肢一覧
1. 現状維持(API Handler で組み立て)
// API Handler
import { getDb } from "../infrastructure/database";
import { createUserRepository } from "../infrastructure/repositories/userRepository";
const userRepository = createUserRepository(getDb());
| メリット | デメリット |
|---|---|
| シンプル | API層が DB の存在を知っている |
| 追加のファイル不要 | Repository 増加で同じパターンが散らばる |
2. Provider パターン
// API Handler
import { provideUserRepository } from "../infrastructure/repositories/userRepositoryProvider";
const userRepository = provideUserRepository();
| メリット | デメリット |
|---|---|
| API層が getDb を知らない | Provider が Repository ごとに必要 |
| シンプルな呼び出し | 依存関係が分散する |
3. Composition Root パターン(今回採用)
// infrastructure/container/index.ts
export const getContainer = (() => {
let container = null;
return () => {
if (!container) {
const db = getDb();
container = {
userRepository: createUserRepository(db),
};
}
return container;
};
})();
// API Handler
const { userRepository } = getContainer();
| メリット | デメリット |
|---|---|
| 依存関係が1箇所に集約 | 初期設定が必要 |
| API層が getDb を知らない | Container が肥大化する可能性 |
| 追加時は Container のみ修正 |
4. Middleware パターン(Hono/Express)
// middleware
app.use("*", async (c, next) => {
c.set("userRepository", createUserRepository(getDb()));
await next();
});
// API Handler
app.post("/", (c) => {
const userRepository = c.get("userRepository");
});
| メリット | デメリット |
|---|---|
| フレームワークと統合 | 型安全性が弱い |
| リクエストごとにインスタンス生成可能 | フレームワーク依存 |
比較表
| パターン | 複雑さ | 集約度 | 型安全性 | 適した規模 |
|---|---|---|---|---|
| 現状維持 | ◎ 低 | × 分散 | ◎ | 小規模 |
| Provider | ○ 中 | △ | ◎ | 小〜中規模 |
| Composition Root | ○ 中 | ◎ 集約 | ◎ | 中規模 |
| Middleware | ○ 中 | △ | △ | 中規模 |
今回 Composition Root を選んだ理由
- 依存関係が1ファイルで把握できる
- ライブラリ不要(純粋な TypeScript)
- 型安全性を維持
- 現在の規模に適切(過剰でも不足でもない)
課題:API層が「組み立て方」を知っている
Before
// users/create/index.ts
import { getDb } from "../../../../../infrastructure/database";
import { createUserRepository } from "../../../../../infrastructure/repositories/userRepository";
const app = new Hono().post("/", async (c) => {
const userRepository = createUserRepository(getDb());
// ...
});
この実装には以下の問題がありました:
API Handler
│
├── import ──► getDb(DB接続の取得方法を知っている)
│
└── import ──► createUserRepository(Repositoryの組み立て方を知っている)
API Handler の本来の責務は「HTTPリクエストを受け取り、レスポンスを返す」ことです。しかし、上記の実装では:
const userRepository = createUserRepository(getDb());
const commentRepository = createCommentRepository(getDb());
- 全ての API ルートで getDb() を import して呼び出す
- Repository の生成方法が変わったら全箇所を修正
解決策:Composition Root パターン
概要
Composition Root とは、アプリケーションの依存関係を 1箇所で組み立てる パターンです。
┌─────────────────────────────────────────────┐
│ Composition Root(Container) │
│ │
│ 全ての依存関係をここで組み立てる │
│ - getDb() の呼び出し │
│ - Repository の生成 │
│ - 将来的には他のサービスも │
└─────────────────────────────────────────────┘
infrastructure/container/index.ts(新規作成)
import { getDb } from "../database";
import { createUserRepository } from "../repositories/userRepository";
import type { IUserRepository } from "../../domain/users/repositories";
export type Container = {
userRepository: IUserRepository;
};
export const getContainer = (): Container => {
let container: Container | null = null;
if (!container) {
const db = getDb();
container = {
userRepository: createUserRepository(db),
};
}
return container;
};
ポイント
- 遅延初期化(初回呼び出し時に生成)
- シングルトン(2回目以降は同じインスタンスを返す)
- 型定義により、何が取得できるか明確
users/create/index.ts(修正後)
import { getContainer } from "../../../../../infrastructure/container";
const app = new Hono().post("/", async (c) => {
const { userRepository } = getContainer();
// ...
});
After
API Handler
│
└── import ──► getContainer
│
├──► getDb
└──► createUserRepository
API層が DB の存在を知らなくなりました。
ディレクトリ構造
infrastructure/
├── auth0/
│ └── index.ts
├── database/
│ └── index.ts # getDb()
├── repositories/
│ ├── index.ts
│ └── userRepository/
│ ├── index.ts # createUserRepository(db)
│ └── index.test.ts
└── container/
└── index.ts # getContainer() ← NEW
複数の Repository が必要な場合
Container を拡張し、必要なものだけ分割代入で取り出します:
// Container の拡張
export type Container = {
userRepository: IUserRepository;
postRepository: IPostRepository;
commentRepository: ICommentRepository;
};
export const getContainer = (): Container => {
if (!_container) {
const db = getDb();
_container = {
userRepository: createUserRepository(db),
postRepository: createPostRepository(db),
commentRepository: createCommentRepository(db),
};
}
return _container;
};
テスタビリティ
Repository 自体は依然として db を外部から受け取る設計のため、テスト時はモックを注入できます:
// テスト
it("ユーザーを保存できる", async () => {
const mockDb = createMockDb();
const repo = createUserRepository(mockDb); // モック注入
// ...
});
Container はプロダクションコードでの「組み立て」を担当し、 テストでは直接 Repository を生成する。 この分離により、テスタビリティを維持しています。
検討事項(TODO)
以下の機能は、現時点では YAGNI(You Aren't Gonna Need It)の原則に従い実装を見送りました。 必要になったタイミングで検討します。
1. 再構築(Reconstitution)
概要: DB から取得した既存データを Entity に復元するための専用 Factory。
- 現状: findUserIdBySub() は ID のみを返すため不要
- 導入タイミング:
findBySub(): Promise<User | null>のように Entity を返す場合
2. Entity の振る舞い拡張
概要: Entity に対する変更操作を追加。
- 現状: toJSON() のみで十分
- 導入タイミング: 名前変更、メール変更などのユースケースが必要になったとき
// 将来の実装イメージ
export type User = {
toJSON: () => { ... };
rename: (newName: string) => User; // イミュータブル
changeEmail: (newEmail: string) => User;
};
3. トランザクション管理
概要: 複数の Repository をまたぐ操作を1つのトランザクションで実行。
- 現状: 単一 Repository への単一操作のみで不要
- 導入タイミング: 複数 Repository をまたぐ処理が必要になったとき
// 将来の実装イメージ
export const createTransactionManager = (db: DatabaseClient) => ({
async run<T>(fn: (tx: Transaction) => Promise<T>): Promise<T> {
return await db.transaction(fn);
},
});
// UseCase での使用
const createUserUseCase = async (input, deps) => {
return await deps.transactionManager.run(async (tx) => {
const user = await deps.userRepository.save(user, tx);
// ...
});
};
まとめ
| 項目 | Before | After |
|---|---|---|
| API層の依存 | getDb, createUserRepository | getContainer のみ |
| 組み立ての責務 | API Handler | Container |
| 依存関係の把握 | 各 API Handler に分散 | 1ファイルに集約 |
| テスタビリティ | ✅ | ✅(変わらず) |
Composition Root パターンにより、依存関係が集約され、API層がよりクリーンになりました。