【実践】DDDのドメインモデル設計手順:集約・値オブジェクト・不変条件をJavaで実装する
この記事でわかること
- ドメインモデルを「どう切って、どう守るか」の設計手順(迷いどころの意思決定)
- 集約(Aggregate)を壊さないためのルールとチェックリスト
- Javaで実装する際の型設計(値オブジェクト / ID / 例外 / ファクトリ)
対象読者:DDDの用語は知っているが、実務で「具体的にこの形で作ればよい」を固めたい中級者
前提:ドメインモデル設計のゴールは“変更に強いビジネスルールの箱”を作ること
DDDでドメインモデルを作る目的は、クラス図を美しくすることではありません
ビジネスルール(不変条件)を一箇所に閉じ込め、破られない形で進化させることがゴールです
そのために本記事では、次の順で設計します
- 言葉(ユビキタス言語)を揃える
- 不変条件(守るべきルール)を列挙する
- 集約境界を決める(トランザクション境界)
- 値オブジェクトで型を強くする
- ドメインサービスの出番を明確にする
- リポジトリは“集約単位”で扱う
- アプリケーション層でユースケースを組み立てる
1. ユビキタス言語:クラス設計より先に「名詞・動詞・制約」を書く
中級者がハマりやすいのは、いきなりエンティティを作り始めることです
先にやるのは、以下の3点の棚卸し
- 名詞:顧客、注文、注文明細、商品、支払い、クーポン…
- 動詞:注文する、支払う、キャンセルする、適用する…
- 制約:支払い前のみキャンセル可能/クーポンは注文合計が○円以上で適用…
この「制約」こそがドメインモデルに閉じ込めるべきものです
2. 不変条件(Invariant)を列挙する:設計の軸はここ
ドメインモデル設計の中心は不変条件です。
例(注文ドメイン):
- 注文は 支払い完了後 に注文明細を変更できない
- 注文合計は「明細の合計」から必ず計算される(手入力させない)
- キャンセルは「発送前」のみ可能
- クーポン適用には下限金額がある
ここまで出たら、次が集約設計です
3. 集約(Aggregate)設計:「一緒に守るべき不変条件」で境界を切る
集約は「関連があるから一緒」ではなく、同時に整合性を保ちたいルールがあるから一緒です
集約を切る基準(実務で効くやつ)
- 同一トランザクションで必ず整合性が必要? → Yesなら同一集約候補
- 外部から参照される入口は1つにできる? → 集約ルート(Aggregate Root)
- 大きすぎてロックや性能が厳しい? → 分割 or 結果整合へ
たとえば「注文」と「支払い」は密に関係しますが、必ずしも同一集約にする必要はありません
支払いが外部決済で非同期なら、注文集約とは別にして 結果整合(イベント/ステータス遷移) にする方が安全なこともあります
4. 値オブジェクト(Value Object)で「型」にルールを持たせる
文字列やintで表すほど、バグが混入します
悪い例
- String email
- int amount
- String orderId
良い例
- Money
- OrderId
値オブジェクトは以下を徹底すると効果が大きいです
- 不変(immutable)
- 生成時にバリデーション
- equals/hashCodeは値で比較
Java例:値オブジェクト(Email / Money)
import java.util.Objects;
public final class Email {
private final String value;
private Email(String value) {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("invalid email");
}
this.value = value;
}
public static Email of(String value) {
return new Email(value);
}
public String value() { return value; }
@Override public boolean equals(Object o) {
return (o instanceof Email other) && Objects.equals(value, other.value);
}
@Override public int hashCode() { return Objects.hash(value); }
}import java.math.BigDecimal;
import java.util.Objects;
public final class Money {
private final BigDecimal amount;
private Money(BigDecimal amount) {
if (amount == null || amount.signum() < 0) throw new IllegalArgumentException("amount must be >= 0");
this.amount = amount;
}
public static Money of(BigDecimal amount) { return new Money(amount); }
public Money add(Money other) { return new Money(this.amount.add(other.amount)); }
public BigDecimal amount() { return amount; }
@Override public boolean equals(Object o) {
return (o instanceof Money other) && amount.compareTo(other.amount) == 0;
}
@Override public int hashCode() { return Objects.hash(amount.stripTrailingZeros()); }
}5. エンティティ:IDとライフサイクルを持つ「同一性のあるもの」だけにする
エンティティは増やしすぎると設計が崩れます
「同一性が必要」なものに限定するのがコツです
- 注文(Order)
- IDがあり状態遷移がある → エンティティ
- 注文明細(OrderLine)
- 集約内の構成要素だが同一性が必要ならエンティティ、不要なら値オブジェクトでもよい
6. 集約ルートに「振る舞い」を集める:貧血モデルを避ける書き方
よくある失敗は、アプリケーション層で if 文だらけにして、エンティティは getter/setter だけ
代わりに、不変条件を守る操作は集約ルートのメソッドとして提供します
Java例:Order集約(ルールを中に閉じ込める)
public class Order {
private final OrderId id;
private OrderStatus status;
private final List<OrderLine> lines = new ArrayList<>();
public Order(OrderId id) {
this.id = id;
this.status = OrderStatus.DRAFT;
}
public void addLine(OrderLine line) {
if (status != OrderStatus.DRAFT) {
throw new DomainException("cannot modify after confirmed");
}
lines.add(line);
}
public void confirm() {
if (lines.isEmpty()) throw new DomainException("order must have at least one line");
status = OrderStatus.CONFIRMED;
}
public Money totalPrice() {
Money total = Money.of(java.math.BigDecimal.ZERO);
for (OrderLine line : lines) total = total.add(line.subtotal());
return total;
}
}ポイントは「状態を直接 set させない」こと
外からの変更入口を制限するのが集約の価値です
7. ドメインサービス:エンティティ/値オブジェクトに置けない「ドメインの計算」だけ置く
ドメインサービスを多用すると手続き型に戻ります
出番はこの条件のときだけ
- 複数集約にまたがる計算・判断
- 主語が“モノ”ではなく「ルール」になる
- エンティティに入れると責務が不自然
例:送料計算、与信判定、割引最適化など
8. リポジトリ:集約ルート単位で永続化する
DDDのリポジトリは「テーブルのDAO」ではなく、集約の出し入れ口です
OrderRepository#findById(OrderId id)OrderRepository#save(Order order)
注文明細だけを個別に更新する、のような入口を増やすと集約が壊れやすくなります
9. アプリケーション層(ユースケース層):トランザクションを組み立てるだけ
アプリ層の役割は次です
- リポジトリから集約を取得
- 集約のメソッドを呼ぶ
- 保存する
- 外部I/O(決済、通知)を呼ぶ(必要ならドメインイベント)
判断ロジックをアプリ層に散らさないのが設計のコツです
10. 「こう設計する」チェックリスト(ここが実務で効く)
最後に、ドメインモデルをレビューする際のチェック例を紹介します
集約
- 集約内の不変条件が集約ルートのメソッドで守られている
- 集約外から内部エンティティを直接更新できない
- リポジトリは集約ルート単位になっている
値オブジェクト
- 文字列/intで表している概念に、VO化できるものが残っていない(Email, Money, Idなど)
- VOは不変で、生成時バリデーションがある
レイヤリング
- ビジネス判断がアプリ層のif文に散っていない
- ドメインサービスは“本当に必要なときだけ”になっている
11. まとめ
よくあるアンチパターン
- 集約が巨大化して全部を1トランザクションに詰め込む
- Entity = DBテーブルで設計してしまう(ドメインの制約が薄まる)
- VOを作らずString地獄(バリデーションが分散)
- アプリ層if文地獄(貧血モデル化)
設計順を守ると迷いが減る
ドメインモデル設計は、結局のところ
- 不変条件を列挙し
- それを守る単位で集約を切り
- 値オブジェクトで型にルールを寄せ
- 集約ルートに振る舞いを集める
これだけで再現性が上がります
ぜひご参考ください!
是非フォローしてください
最新の情報をお伝えします
