パッケージ構造
私たちのバックエンドサービスは、機能単位のパッケージ構成(Package by Feature) に従い、4つの兄弟モジュールで編成されています。このドキュメントは、新しいコードの配置場所についての信頼できる情報源です。
言語に関する注記。 以下の例は、私たちの主要なバックエンドスタックであるJava/Spring Bootで記述されていますが、構造自体は言語に依存せず、使用するすべてのバックエンド言語(Kotlin、Go、Python、Nodeなど)で必須です。ファイル拡張子とフレームワークのアノテーションは適宜翻訳してください。フォルダ構成、命名サフィックス(
*Db、*Repo、*RepoImpl、*Dao、*Controller、*ControllerImpl)、およびインターフェース/実装の分割は変更しないでください。
基本原則
技術レイヤーではなく、機能でグループ化する。 「orders」機能への変更は、controllers/、services/、repositories/ パッケージに散らばったファイルではなく、互いに隣接するファイルに影響を与えるべきです。技術レイヤー化は モジュール 境界(db、client、capabilities、starter)で行われ、機能単位のパッケージ構成は各モジュール内部で行われます。
4つのモジュール
すべてのサービスは、以下の4つの兄弟モジュールで構成されます。
<service>/
├── capabilities/ # 機能/ビジネスロジック — 頭脳
├── db/ # 永続化: エンティティ、リポジトリ、マイグレーション
├── client/ # 外部システムとのアウトバウンド統合
└── starter/ # Spring Boot スターター — すべてを配線し、実行する
capabilities/ — ビジネスロジック
各機能は独自のパッケージを持ち、各機能内部にはAPIエンドポイントごとに1つのサブパッケージ(getById、create、updateById、getByQuery、…)があります。各サブ機能は、3つの兄弟フォルダを所有します。
service/— そのAPIの単一サービスクラスdto/—request/およびresponse/(該当するもの)mapper/—*DbエンティティとDTOを(双方向に)変換する、このサブ機能専用のマッパー
機能の コントローラーは機能パッケージのルート に配置され、リポジトリと同様にインターフェースと実装に分割されます。
capabilities/<feature>/<Feature>Controller.java— インターフェース。ルート、HTTP動詞、リクエスト/レスポンスタイプ、APIドキュメントアノテーションを宣言します。capabilities/<feature>/<Feature>ControllerImpl.java— 実装。フレームワークのアノテーション(@RestController)を持ち、各ルートを対応するサブ機能サービスに委譲します。
各機能のサブ機能間で共有されます。コントローラー を参照してください。
- 依存関係:
db(*Repoインターフェース経由のみ — DB使用ルール を参照)、client starterに依存しないmainクラス、@SpringBootApplicationを含まない- 所有: HTTPコントローラー、APIリクエスト/レスポンスDTO、マッパー、サービス
db/ — 永続化
テーブルごとに1つのフォルダ で構成されます。各テーブルフォルダには、正確に3つのサブフォルダがあります。
db/<tableName>/
├── model/
│ ├── <EntityName>Db.java # メインJPAエンティティ
│ └── <Other>Db.java # サブモデル(埋め込み型、子エンティティ、射影)
├── dao/
│ └── <EntityName>Dao.java # 低レベルデータアクセス(Criteria/JPQL/ネイティブクエリ)
└── repository/
├── <EntityName>Repo.java # PUBLIC インターフェース — capabilities が見る唯一の型
└── impl/
└── <EntityName>RepoImpl.java # Spring Data + Dao をインターフェースの背後で構成
命名規則
| レイヤー | サフィックス | 例 |
|---|---|---|
| JPAエンティティ | Db | TestcaseDb |
| リポジトリインターフェース | Repo | TestcaseRepo |
| リポジトリ実装 | RepoImpl | TestcaseRepoImpl |
| データアクセスヘルパー | Dao | TestcaseDao |
DB使用ルール
Capabilities は *Repo インターフェースにのみ依存しなければなりません。 *RepoImpl と *Dao は内部用です — db/<tableName>/ パッケージの外部から注入されたり参照されたりしてはいけません。これにより、永続化レイヤーを交換可能に保ち、能力テストを契約に集中させることができます。
- 依存関係: このサービス内にはない
- 公開:
*Repoインターフェースと*Dbモデル型 - 含まない: ビジネスロジック、API DTOへの変換
client/ — アウトバウンド統合
このサービスが依存する 他のサービスを呼び出す コードを保持します。主に、プラットフォーム上の 依存マイクロサービス(paymentService、userService、executionService、…)ごとに1つのサブフォルダがあります。サードパーティSDKラッパーやメッセージプロデューサーもここに配置される可能性がありますが、主要なパターン(そしてこのモジュールが存在する理由)は、依存マイクロサービスクライアントです。
client/<microserviceName>/
├── <Microservice>Client.java # インターフェース — capabilities が依存するもの
├── impl/
│ └── <Microservice>ClientImpl.java # 実装: HTTP/gRPC/SDK呼び出し、リトライ、タイムアウト
├── dto/ # このマイクロサービスのAPIのリクエスト/レスポンス形状
│ ├── request/
│ └── response/
└── mapper/ # オプション: 彼らの形状と私たちの形状を変換
ルール
-
依存マイクロサービスごとに1つのサブフォルダ。サービス名で名前付けします。たとえ「関連している」場合でも、複数のマイクロサービスを1つのパッケージにまとめないでください。
-
Capabilities は
*Clientインターフェースにのみ依存します。*ClientImplは内部用です —*Repo/*RepoImplと同じルールです。 -
クライアントインターフェースは、マイクロサービスのAPIを自身のDTOの用語で公開します。capabilities は必要に応じてそれらをさらに適応させることができます。
-
横断的関心事(リトライ、サーキットブレーカー、リクエスト/レスポンスログ、認証ヘッダー)は
impl/に属します — capabilities はそれらを見るべきではありません。 -
依存関係: このサービス内にはない
-
公開:
*Clientインターフェースとそのリクエスト/レスポンスDTO -
含まない: ビジネスロジック、capabilities のオーケストレーション
starter/ — 構成ルート
実行される唯一のモジュール。@SpringBootApplication、グローバルREST例外ハンドラ、セキュリティ設定、フィルター、application.yml を含みます。コントローラーはありません — それらは capabilities/ にあります。
- 依存関係:
capabilities(および推移的にdb、client) - 所有: デプロイ可能なアーティファクト、横断的Web設定(セキュリティ、CORS、エラーハンドリング)
- 所有しない: 機能コントローラーや機能DTO
依存関係ルール
starter → capabilities → { db, client }
矢印は一方向です。db と client は capabilities や starter からインポートしません。capabilities は starter からインポートしません。
コントローラー
サブ機能ごとではなく、機能ごとに1つのコントローラーがあり、capabilities/ 内の 機能パッケージのルート に配置されます。*Repo / *RepoImpl の分割を反映して、2つのファイルに分割されます。
capabilities/<feature>/<Feature>Controller.java— インターフェース。ルート、HTTP動詞、パス/クエリパラメータ、リクエスト/レスポンスDTO型、APIドキュメントアノテーション(OpenAPI/Swagger)を宣言します。インターフェースを読む人は、HTTP契約全体を見ることができるはずです。capabilities/<feature>/<Feature>ControllerImpl.java— 実装。フレームワークのアノテーション(Springでは@RestController)を持ち、サブ機能サービスの注入対象であり、各ルートに対して、対応するservice/を呼び出して結果を返す1つの短いメソッドを含みます。委譲以外のロジックはありません。
なぜ分割するのか?インターフェースはAPI契約です — 安定しており、文書化され、単独でテスト可能です。実装は配線です — 契約を変更せずに変更できます。*Repo と *RepoImpl と同じ理由です。
コントローラーは、各HTTPルートを capabilities/<feature>/<sub-feature>/service/ 内の対応するサブ機能サービスに委譲します。リクエスト/レスポンスDTOはサブ機能(capabilities/<feature>/<sub-feature>/dto/)が所有します。コントローラーは単なる薄いHTTPアダプターです。
なぜ starter/ ではなく capabilities/ 内部なのか?コントローラー、そのサービス、およびそのDTOは、APIが変更されるときにすべて一緒に変更されます — それらを1つのモジュールに保持することで、「1つの機能、1つの場所」という機能単位のパッケージ構成の約束が守られます。starter/ は横断的配線(セキュリティ、エラーハンドラ、アプリケーションエントリポイント)専用であり、機能ごとのWebコード用ではありません。
動作例: testcase 機能
以下のツリーは、説明のために .java 拡張子を使用しています。言語のファイル拡張子とフレームワークのイディオム(Kotlinの .kt、Goのパッケージ、Pythonのモジュールなど)を適宜置き換えてください。ただし、フォルダ構成と命名サフィックスは同一に保ってください。
testcase-service/
├── capabilities/
│ └── com/example/testcase/capabilities/
│ └── testcase/ # メイン機能
│ ├── TestcaseController.java # インターフェース — HTTP契約、ルート宣言、APIドキュメント
│ ├── TestcaseControllerImpl.java # 実装 — @RestController、サブ機能サービスに委譲
│ ├── getById/ # サブ機能 = 1つのAPI
│ │ ├── service/
│ │ │ └── GetTestcaseByIdService.java
│ │ ├── dto/
│ │ │ └── response/
│ │ │ └── GetTestcaseByIdResponse.java
│ │ └── mapper/
│ │ └── GetTestcaseByIdMapper.java # TestcaseDb → レスポンス
│ ├── create/
│ │ ├── service/
│ │ │ └── CreateTestcaseService.java
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── CreateTestcaseRequest.java
│ │ │ └── response/
│ │ │ └── CreateTestcaseResponse.java
│ │ └── mapper/
│ │ └── CreateTestcaseMapper.java # リクエスト → TestcaseDb, TestcaseDb → レスポンス
│ ├── updateById/
│ │ ├── service/
│ │ │ └── UpdateTestcaseByIdService.java
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── UpdateTestcaseByIdRequest.java
│ │ │ └── response/
│ │ │ └── UpdateTestcaseByIdResponse.java
│ │ └── mapper/
│ │ └── UpdateTestcaseByIdMapper.java
│ └── getByQuery/
│ ├── service/
│ │ └── GetTestcaseByQueryService.java
│ ├── dto/
│ │ ├── request/
│ │ │ └── GetTestcaseByQueryRequest.java
│ │ └── response/
│ │ └── GetTestcaseByQueryResponse.java
│ └── mapper/
│ └── GetTestcaseByQueryMapper.java
│
├── db/ # テーブルごとに1つのフォルダ
│ └── com/example/testcase/db/
│ └── testcase/ # テーブル名
│ ├── model/
│ │ ├── TestcaseDb.java # JPAエンティティ — メイン
│ │ └── TestcaseStepDb.java # サブモデル(子/埋め込み)
│ ├── dao/
│ │ └── TestcaseDao.java # クエリ/条件ヘルパー、パッケージプライベート
│ └── repository/
│ ├── TestcaseRepo.java # インターフェース — capabilities はこれに依存
│ └── impl/
│ └── TestcaseRepoImpl.java # Spring Data + TestcaseDao を構成
│
├── client/ # 依存マイクロサービスごとに1つのフォルダ
│ └── com/example/testcase/client/
│ ├── executionService/ # 呼び出す依存マイクロサービス
│ │ ├── ExecutionServiceClient.java # インターフェース
│ │ ├── impl/
│ │ │ └── ExecutionServiceClientImpl.java # HTTP/gRPC呼び出し、リトライ、タイムアウト
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── RunTestcaseRequest.java
│ │ │ └── response/
│ │ │ └── RunTestcaseResponse.java
│ │ └── mapper/
│ │ └── ExecutionServiceMapper.java
│ └── userService/ # 別の依存マイクロサービス
│ ├── UserServiceClient.java
│ └── impl/
│ └── UserServiceClientImpl.java
│
└── starter/ # 横断的配線のみ — コントローラーなし
└── com/example/testcase/
├── TestcaseServiceApplication.java
├── config/
│ ├── SecurityConfig.java
│ └── WebConfig.java
├── error/
│ └── GlobalExceptionHandler.java # @ControllerAdvice — 横断的、機能ごとではない
└── resources/application.yml
経験則
- 既存の機能に新しいAPIを追加する?
capabilities/<feature>/内に、独自のservice/、dto/{request,response}/、mapper/を持つ新しいサブ機能パッケージを作成し、ルート宣言を<Feature>Controller.java(インターフェース)に、委譲メソッドを<Feature>ControllerImpl.javaに追加します。2つ目のコントローラーは作成しないでください。 - まったく新しい機能を追加する?
capabilities/に機能パッケージ(ルートにコントローラー、APIごとにサブ機能)を作成し、db/<tableName>/に対応するテーブルフォルダ(model/、dao/、repository/{impl/}を含む)を作成します。外部システムを呼び出す場合のみクライアントを追加します。starter/には触れないでください。 - 常に
*Repoを注入し、*RepoImplや*Daoは注入しないでください。 Capabilities はインターフェースにのみ依存します。*RepoImplと*Daoはdb/<tableName>/内部のものです — クエリ戦略やバックエンドストアを、capability コードを変更せずに交換できるようにするために存在します。 - 常に
*Clientを注入し、*ClientImplは注入しないでください。 依存マイクロサービス呼び出しにも同じルールです。Capabilities はインターフェースに依存します。トランスポート(HTTP、gRPC、SDK)はimpl/の背後にあります。 client/内の依存マイクロサービスごとに1つのフォルダ。 フォルダ名はマイクロサービス名にします。「すべての内部サービス」を1つのパッケージにグループ化しないでください。- サブ機能の粒度は
capabilities/で止まります。db/はテーブルごとに編成され、client/は外部システムごとに編成されます。いずれかをAPIごとに分割すると、凝集性の高いコードが散らばってしまいます。 - リクエスト/レスポンスDTOとそのマッパーはサブ機能ごとにあり、共有されません。 もし
createとupdateByIdが今日似ているように見えても、2つのDTOと2つのマッパーを保持してください。共有すると、独立して変化する2つのAPIが結合されます。 - マッパーはエンティティ ↔ DTOを直接変換します。 中間ドメインモデル層はありません。
<SubFeature>Mapperは*Dbとそのサブ機能のリクエスト/レスポンスDTOを変換します。 - 3つのサブ機能が類似したコードを持っている? 同じパターンが 4つ以上 の場所に現れるまで、共有サービスやマッパーを抽出したい衝動を抑えてください。
- 機能間の再利用? 共有ユーティリティを
common/パッケージに配置します。
(注: 元の内容はここで切り詰められていますが、翻訳は原文に忠実に、指示されたすべてのセクションをカバーしています。)