1. 背景
DevlimeのPrismでScrumのAgileProject(スプリント実行単位)とCrewitのProject(契約および工数管理)に関する情報をまとめて表示するため、2つのドメイン間に関係を設定する必要が生じました。また、2つのドメインの関係を設定する画面で、Crewitの工数情報を表示する必要もありました。
これに伴い、共有APIという新しいレイヤーが追加されました。このレイヤーを活用して2つのMSのドメイン間の関係を定義し、一方のMSのドメインを共有ドメインとして設計することで、他のMSでもそのドメインを利用できるようにしました。
本書は、上記の事例をきっかけに整理した共有ドメインおよびMS間のドメイン参照に関する設計原則と方法を共有し、今後新たな共有ドメインを設計する際の参考とすることを目的としています。
2. 設計判断基準
MS間でドメインをマッピングする必要がある状況は、共有APIの有無とパフォーマンス問題の発生有無によって以下の3つに分かれ、それぞれ適用すべき設計が異なります。実際の設計に入る前に、まずどのケースに該当するかを判断する必要があります。
|
シナリオ |
条件 |
設計方式 |
|---|---|---|
|
1 |
共有APIがなく、2つのMSドメイン間のマッピングが必要な場合 |
参照するMSが、元のMSのドメインをエンティティ形式で直接保持する |
|
2 |
共有APIが存在し、パフォーマンス問題が大きく発生しない場合 |
参照するMSは元のドメインのIDのみを保持し、必要に応じて共有APIを通じてIDで取得する |
|
3 |
共有APIが存在し、パフォーマンス問題(N+1、繰り返し呼び出しなど)が発生する場合 |
参照するMSが表示用VOを保持し、バッチ・遅延・ビジネスロジックによって更新する |
3. 設計原則
|
共有ドメイン設計 |
MS間参照設計 |
|
|---|---|---|
|
例 |
共有APIの共有ドメインエンティティ |
scrumのAgileProjectが保持するcrewitのProject参照 |
|
位置 |
独立した共有APIサービス内部 |
参照する側のサービス(scrum)のドメイン内部 |
|
性質 |
元のMSドメインのプロジェクション |
元のドメインとは別の独立したオブジェクト、または元のMSドメインのId |
3-1. 共有ドメイン設計原則
共有ドメインとは、元のMSのドメインのうち、他のMSと共有する目的で存在するドメインです。以下の原則に従います。
1. 元のドメインと同じライフサイクルを持つ
元のMSで保存・変更が発生すると、イベントを通じて共有ドメインも同時に更新されます。つまり、元のドメインと同じライフサイクルになるようにします。元のMSの既存のDataEvent → ProjectionHandlerフックにトリガーを設定して反映します。
2. 共有に必要なフィールドのみを含める
元のフィールドをすべて複製することはしません。元のMS内部の管理用フィールドは除外し、他のMSが実際に必要とするフィールドのみを含めます。
3. 名称には元の用語をそのまま使用しない
元のMSのエンティティ名をそのまま使用すると、他のMSから見たときに意味が曖昧になります。実際の主要な属性を反映して再定義します。
3-2. MS間参照設計原則
あるMSのドメインが他のMS(または共有ドメイン)の情報を参照することで、関係を表現する必要がある場合です。具体的にどのように参照するかはシナリオによって異なるため、以下ではシナリオごとに分けて説明します。
3つのシナリオすべてに共通して適用される原則は、次の2つです。
1. 伝播されたデータは元のドメインとは別のオブジェクトとして扱う
参照する側が保持する情報(エンティティ、VO)は、元のフィールドの複製ではなく、参照ドメインが実際に必要とする属性のみを含む独自定義のオブジェクトです。
2. 名称は参照するドメインの観点から再定義する
元の用語をそのまま使うのではなく、参照するドメインの立場から、この情報が実際にどのような意味を持つのかを反映して命名します。
3-2-1. シナリオ 1 — 共有 API なし:エンティティで直接保有
共有 API がまだ存在しない状態で 2 つの MS ドメインをマッピングする場合、参照する MS が元のドメイン情報を自身のエンティティとして直接保有します。実質的に、共有ドメインの役割を参照する MS 内部で持つことになります。
1. 共有ドメインと同様に、sourceSystem/sourceKey によって追跡可能性を確保します。
2. 可能であれば元のイベントを受け取って更新し、それが難しい場合は手動/バッチ更新の経路を用意します。
3. 参照する MS が 2 つ以上に増えると、それぞれが似たようなコピーを個別に作成することになり、管理が難しくなります。その時点で共有 API を作成し、拡張性のある構成にします。
3-2-2. シナリオ 2 — 共有 API あり、パフォーマンス問題なし:ID 参照(基本)
共有 API があり、繰り返し参照によるパフォーマンス問題が確認されていない場合、参照する MS は ID のみを保有し、参照するたびに共有 API をリアルタイムで呼び出します。
1. VO フィールドは持たず、参照対象のドメインの参照キー(ID)1 つだけをフィールドとして保有します。
2. 参照時点で共有 API をリアルタイムに呼び出し、参照キーを通じてマッピングされているドメインを取得します。
3-2-3. シナリオ 3 — 共有 API あり、パフォーマンス問題発生:VO(例外)
シナリオ 2 で運用している中で、繰り返し参照によるパフォーマンス問題が実際に確認された場合、参照する MS が表示用 VO を保有し、バッチ再同期または遅延(lazy)更新によって値を設定します。
1. 同期自体を避ける
リアルタイムの強制同期(元の変更を即座に最新化すること)は採用しません。
2. 整合性の問題はビジネスロジックで解決する
VO は表示専用としてのみ使用し、実際にビジネス上の判断が必要なロジックでは VO の値を信頼せず、参照キーによって元のドメイン(または共有ドメイン)を再取得します。
3. syncedAt によって最新性を可視化する
最後に同期した時刻も併せて公開し、データの鮮度を確認できるようにします。
更新方式
|
方式 |
内容 |
特徴 |
|---|---|---|
|
バッチ再同期 |
スケジューラーが定期的に(例:1 日 1 回)再取得して VO を更新 |
インフラの追加を最小限に抑え、整合性の遅延幅がバッチ周期分だけ固定的に発生することを正常な状態として受け入れる |
|
遅延(lazy)更新 |
scrum が AgileProject を扱う時点(作成/更新など)で、syncedAt が古くなっていれば、その時点で再取得する |
バッチインフラは不要。ただし、しばらくアクセスがない場合は、最新化されていない状態のまま残り続ける可能性がある |
必ずしも上記 2 つの方式だけを使用する必要はなく、ビジネス上の不一致問題を解決できる別の方法がある場合は、その方法を使用できます。
4. 設計例(シナリオ 2)
① 元のドメイン(crewit の Project)
public class Project extends StageEntity {
...
private String code; // 코드
private LangStrings langNames; // 이름
private LangStrings langDescription; // 설명
private LangStrings langCustomers; // 고객사
private LangStrings langLocations; // 위치
private Member manager; // PM
private ProjectStatus projectStatus; // 프로젝트상태
private ProjectPeriod projectPeriod; // 프로젝트기간
private String skillSet; // 기술세트
private LangStrings langVendors; // 협력업체
private LangStrings langRiskNotes; // 리스크
private double budget; // 예산, ex, 21억5천만원
private int totalEffort; // 총공수, 230 M/M
private boolean active;
transient List<ProjectManMonthPlan> projectManMonthPlans; // 구성원별
transient List<ProjectMonthPlan> projectMonthPlans; // 월별
transient List<ManOutput> manOutputs;
transient List<ManRatio> manRatios;
transient List<ManOutputMonthSum> manOutputMonthSums;
transient List<ManRatioMonthSum> manRatioMonthSums;
...
}
② 共有ドメイン(共有 API の Staffing Project — Project の Projection)
public class StaffingProject extends StageEntity {
...
private String id; // PK
private String sourceSystem; // 원본 MS 또는 도메인 추적용
private String sourceKey; // crewit Project의 Id
private String code; // 코드
private LangStrings name; // 이름
private LangStrings description; // 설명
private LangStrings customerNames; // 고객사
private ProjectStatus status; // 프로젝트상태
private ProjectPeriod projectPeriod; // 프로젝트기간
private Member manager; // 담당자
private double budget; // 예산, ex, 21억5천만원
private int totalEffort; // 총공수, 230 M/M
private boolean active;
private LocalDateTime syncedAt; // 마지막 동기화 시각
...
}
-
crewit Project の保存/更新時に、DataEvent/ProjectionHandler フック+イベントを通じて更新(元のドメインと同じライフサイクル)
-
skillSet/budget/langVendors などの内部管理フィールドは除外
-
名称は「契約情報」よりも「工数情報」が主要な属性であるという議論に基づき、StaffingProject と命名
-
syncedAt で最後に同期した時刻を管理(共有ドメインであっても完全なリアルタイム反映を保証するものではないため、追跡目的で保持)
③ 参照するサービス(scrum の AgileProject)
public class AgileProject extends StageEntity {
...
private String staffingProjectId; // StaffingProject 참조키. ID만 보유.
...
}
-
AgileProject は crewit/共有ドメインの情報をフィールドとして保有しません — ID 1 つだけを参照します。
-
画面表示のたびに共有 API を呼び出して情報を取得します。
5. 参考(シナリオ 3)— パフォーマンス問題発生時への備え
今後、参照頻度が高くなってパフォーマンス問題が確認された場合は、以下のように表示用 VO を埋め込む方式への切り替えを検討します。現在は採用せず、参考用としてのみ記載します。
public class StaffingProject implements ValueObject {
...
private String staffingProjectId; // staffing project id
private LangStrings name; // 이름
private LangStrings customerNames; // 고객사
private ProjectStatus status; // 프로젝트상태
private ProjectPeriod projectPeriod; // 프로젝트기간
private Member manager; // 담당자
private double budget; // 예산, ex, 21억5천만원
private int totalEffort; // 총공수, 230 M/M
private LocalDateTime syncedAt; // 마지막 동기화 시각
...
}
-
AgileProject に含めて関係を指定
-
crewit の複製ではなく、scrum の別個の独立したオブジェクトとして管理
-
元のドメインをリアルタイムに反映せず、バッチ再同期または遅延更新(あるいは別のビジネスロジック)によって値を設定
-
切り替え時点は、実際の参照パターンでパフォーマンス問題が確認された後に限定
Bignow