アーキテクチャ
構造は、変更の影響と業務上の制約を局所化するために導入します。ディレクトリの対称性やパターンの採用自体を目的にしません。
必要になった分だけ分ける
単純な表示、読み取り、CRUDでは、InterfaceからORMや局所的なクエリを直接利用できます。
複数の手順、権限、整合性、トランザクションを一つの操作として扱う必要が生じたら、Applicationへ分離します。業務ルールや状態遷移を複数箇所で守る必要が生じたら、DomainへEntity、Value Object、純粋関数、ステートマシンを置きます。
将来必要になるかもしれない層を先に作りません。一方で、権限や整合性に関わる処理をUIやハンドラへ散らさず、必要と分かった時点で明確な境界へ移します。
各層の責務
Interface
HTTP、CLI、画面など外部との入出力を扱います。入力の実行時検証、認証情報の取得、Applicationの呼び出し、レスポンスへの変換を担当します。画面への到達制御や操作の表示制御は利用者体験のために行いますが、サーバー側の認可を置き換えません。
Application
一つの利用者操作を完了させる手順、対象資源への認可、トランザクション境界を持ちます。関数を基本としますが、リクエスト単位の依存関係や共通の実行形式を保持するプロジェクトでは、一操作をexecuteするクラスを使います。業務判断はDomainへ、技術詳細はInfrastructureへ委ねます。
Domain
業務上の不変条件、計算、状態遷移を表します。I/Oやフレームワークへ依存させません。Entity、Value Object、ステートマシンは、単なるデータ構造や関数より振る舞いと制約を安全に表せる場合だけ導入します。
Infrastructure
データベース、外部API、ファイル、メッセージングなど技術的なI/Oを担当します。外部のエラーやデータ形式を内部の契約へ変換し、詳細を上位層へ漏らしません。変換する契約や隔離する変化がある場合はAdapterまたはGatewayを設けます。
依存関係
アプリケーションの起動点で具体的な依存関係を組み立て、必要な関数やクラスへ明示的に渡します。Dependency InjectionにDIコンテナは必須ではありません。差し替えや境界の明示が実際に必要になったとき、利用側が求める最小の契約を定義します。
弊社の社内プロダクトであるHIRACTでは、HonoのContextをリクエスト単位の依存関係と認証情報を運ぶ入れ物としてApplicationへ渡し、InfrastructureのRepositoryも同じContextから接続先を取得します。Contextはサーバー側のリクエスト処理に限定し、Domain、純粋な計算、クライアントと共有するモデルへは持ち込みません。別のプロジェクトでは、必要なDB、設定、認証情報だけを引数として渡す小さな構成を選べます。
永続化
Repositoryは、業務上まとまりのあるドメインオブジェクトを永続化し、保存規則やデータマッピングを隠す必要がある場合に使います。メソッドは汎用CRUDへ機械的に揃えず、利用側が必要とする意図を表します。
画面表示、検索、集計のための複雑な読み取りは、集約のRepositoryを肥大化させず専用クエリとして実装します。単純な読み取りまで形式的にRepositoryで包みません。
エラーと権限
Infrastructureの技術的な失敗はErrorの値へ変換します。Applicationは判断できる失敗を具体的なエラーに変え、T | Errorとして返します。Interfaceは既知のエラーを外部向けの応答へ変換し、それ以外を共通のエラーハンドラで記録します。
認証情報の取得、画面への到達、操作の表示、APIでの認可、対象資源への認可を混同しません。画面への到達と表示制御は同じ権限情報から導出しますが、認可の正本はAPIとApplicationに置きます。権限情報が不足している、または判定に失敗した場合は、既定で拒否します。
避ける構造
- 単純なCRUDを全層に通すためだけのファイル群
- HonoやNext.jsのContextをDomainや共有モデルへ渡すこと
- 実装の詳細をそのまま写したinterface
- 一つのRepositoryへ更新、検索、集計、画面用DTOを集約すること
- DomainオブジェクトからDBや外部APIを直接呼ぶこと
- 認可判定をクライアント表示だけに任せること