ドメイン駆動設計(DDD:Domain-Driven Design)とは、ソフトウェアが対象とする業務(ドメイン)の知識と言葉を中心に置き、その知識をそのままコードで表現していく設計の考え方です。2003年にEric Evansが著書『Domain-Driven Design』で提唱しました。

DDDは「値オブジェクト」「集約」などの実装パターンが目立つため、「特定のフォルダ構成やレイヤー構成のこと」と誤解されがちです。しかし本来の中心は、業務の専門家と開発者が同じ言葉で話し、その言葉と境界をコードに反映することにあります。この記事では、DDDの全体像を戦略的設計(境界と言葉を決める)と戦術的設計(コードで表す)に分けて整理し、値オブジェクトと集約はJava 21で実際に動かした結果で、「直す前と後」を比べます。最後に、既存のプロジェクトで小さく始める手順を紹介します。

ドメイン駆動設計の用語早見表

分類 用語 ひとことで
戦略的設計 ユビキタス言語 業務の専門家と開発者が共通で使い、コードにもそのまま使う言葉
戦略的設計 境界づけられたコンテキスト 1つの言葉が1つの意味を持つ範囲。モデルとチームの境界
戦略的設計 コンテキストマップ コンテキスト同士の関係(どちらが上流か、どう変換するか)の地図
戦略的設計 コアドメイン 事業の強みになる、最も力を入れるべき領域
戦術的設計 エンティティ IDで区別され、状態が変わっても同じものとして扱う(例:会員、注文)
戦術的設計 値オブジェクト 値そのもので区別され、変更できない(例:金額、期間、住所)
戦術的設計 集約 一緒に整合性を保つオブジェクトのまとまり。変更は入口(集約ルート)を通す
戦術的設計 リポジトリ 集約を保存・取得する窓口。DBの詳細を隠す
戦術的設計 ドメインサービス どのエンティティにも自然に属さない業務ルール
戦術的設計 ドメインイベント 業務上の重要な出来事(例:注文が確定した)
戦術的設計 ファクトリ 複雑な集約の生成をまとめる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「ドメイン駆動設計入門|DDDの基本と小さく始める手順をJavaで解説」

DDDの目的:複雑な業務を、変更に強いコードで表す

Evansの原著の副題は「ソフトウェアの核心にある複雑さに立ち向かう」です。DDDが解決しようとしているのは、技術の複雑さではなく、業務そのものの複雑さです。

DDDを使わないときに起きがちなこと DDDで目指す状態
業務の担当者は「受注」、仕様書は「オーダー」、コードは Request と、同じものの呼び名がばらばら 同じ言葉が、会話・仕様・コードで一致している
業務のルールが、画面・サービス・SQLのあちこちに散らばる ルールがドメインのオブジェクトに集まり、1か所を見ればわかる
1つの巨大なモデル(例:すべての部署の都合が入った Customer クラス)を全員で共有し、変更の影響が読めない 境界ごとにモデルを分け、変更の影響を境界の中に閉じ込める

戦略的設計:言葉と境界を決める

DDDで最初に取り組むべきなのは、コードの書き方より、言葉と境界を決める「戦略的設計」です。

ユビキタス言語

ユビキタス言語とは、業務の専門家と開発者がチーム内で共通に使い、コードのクラス名やメソッド名にもそのまま使う言葉です。

会話で使う言葉 ユビキタス言語がないコード ユビキタス言語を反映したコード
「注文を確定する」 order.setStatus(2) order.confirm()
「在庫を引き当てる」 stockDao.update(id, qty * -1) stock.allocate(quantity)
「会員ランクが上がる」 user.setLevel(user.getLevel() + 1) member.promote()

会話に出てこない言葉がコードにあったり、コードの名前が会話で通じなかったりしたら、言葉を見直すサインです。用語集を作り、仕様の議論で新しい言葉が出たら、コードと一緒に更新します。

境界づけられたコンテキスト

境界づけられたコンテキスト(Bounded Context)とは、1つのモデルと言葉が、一貫した意味を持つ範囲のことです。同じ「商品」という言葉でも、部署によって関心は違います。

コンテキスト 「商品」の意味 必要な情報
カタログ お客さまに見せるもの 名前、説明、画像、表示価格
在庫 倉庫で数えるもの 保管場所、数量、引当済みの数
配送 運ぶもの 重さ、サイズ、配送区分

これを1つの Product クラスにまとめると、どの部署の変更でも同じクラスを触ることになり、影響範囲が読めなくなります。コンテキストごとに別のモデルを持ち、共通なのは商品ID程度にするのがDDDの考え方です。境界は、チームの分け方や、マイクロサービスの分け方を考えるときの有力な手がかりにもなります。

コンテキストマップとコアドメイン

コンテキストマップは、コンテキスト同士の関係を描いた地図です。代表的な関係には、相手のモデルに合わせる「順応者(Conformist)」、変換層を置いて自分のモデルを守る「腐敗防止層(Anti-Corruption Layer)」などがあります。

また、ドメインはコアドメイン(事業の強み)、支援サブドメイン(必要だが差別化にならない)、汎用サブドメイン(認証・メール配信など、既製品で済むもの)に分けて考えます。DDDの手間をかけるのはコアドメインに絞り、それ以外は既製品やシンプルな作りで済ませるのが、現実的な進め方です。

戦術的設計:業務の知識をコードで表す

エンティティと値オブジェクト

エンティティ 値オブジェクト
区別の方法 ID(中身が同じでも、IDが違えば別物) 値(値が同じなら同じもの)
変更 状態が変わる(会員の住所変更など) 変更できない。変えるときは新しい値を作る
例 会員、注文、商品 金額、期間、住所、メールアドレス、数量
Javaでの書き方 通常のクラス。equals はIDで比較 record が向いている

実務で最初に効果が出やすいのは値オブジェクトです。long や String のままだと、「マイナスの金額」「通貨の違う金額の足し算」を防げません。

// 値オブジェクト:金額と通貨をセットで扱い、不正な値を作れないようにする
record Money(long amount, String currency) {
    Money {
        if (amount < 0) throw new IllegalArgumentException("金額は0以上: " + amount);
        Objects.requireNonNull(currency);
    }
    static Money yen(long amount) { return new Money(amount, "JPY"); }
    Money add(Money other) {
        if (!currency.equals(other.currency))
            throw new IllegalArgumentException(currency + " と " + other.currency + " は足せません");
        return new Money(amount + other.amount, currency);
    }
    Money times(int n) { return new Money(amount * n, currency); }
    boolean isGreaterThan(Money other) { return amount > other.amount; }
    @Override public String toString() { return amount + " " + currency; }
}
Money a = Money.yen(1000);
Money b = Money.yen(1000);
System.out.println("値が同じなら等しい: " + a.equals(b));
System.out.println("足し算は新しい値を返す: " + a.add(Money.yen(500)) + " / 元の値: " + a);
try { Money.yen(-1); }
catch (IllegalArgumentException e) { System.out.println("生成時に検証: " + e.getMessage()); }
try { a.add(new Money(10, "USD")); }
catch (IllegalArgumentException e) { System.out.println("通貨違いの計算を防ぐ: " + e.getMessage()); }
値が同じなら等しい: true
足し算は新しい値を返す: 1500 JPY / 元の値: 1000 JPY
生成時に検証: 金額は0以上: -1
通貨違いの計算を防ぐ: JPY と USD は足せません

Money 型の値は、必ず検証を通った正しい値です。受け取る側で毎回チェックする必要がなくなり、メソッドの引数が long price ではなく Money price になることで、意味の取り違えもコンパイル時に防げます。

集約と集約ルート

集約とは、一緒に整合性(業務のルール)を守らなければならないオブジェクトのまとまりです。外から変更するときは、必ず入口である「集約ルート」のメソッドを通します。

直す前:データの入れ物だけのモデル

class Order {                         // データの入れ物だけ(ルールは外のサービスに散らばる)
    public List<OrderLine> lines = new ArrayList<>();
    public String status = "DRAFT";
    public long total;
}
record OrderLine(String product, long unitPrice, int quantity) {}

Order order = new Order();
order.lines.add(new OrderLine("ノート", 300, 2));
order.status = "CONFIRMED";                       // 確定済み

// 別の画面の処理:確定後なのに明細を直接追加できてしまう
order.lines.add(new OrderLine("ペン", 120, 1000));
// 合計の再計算も呼び忘れた
System.out.println("状態=" + order.status + ", 明細数=" + order.lines.size() + ", 合計=" + order.total);
状態=CONFIRMED, 明細数=2, 合計=0

確定済みの注文に明細が追加され、合計も0円のままです。「確定後は変更できない」「合計は明細から計算する」というルールが、Order の外(各サービス)に任されているため、1か所でも守り忘れると、ありえない状態のデータが生まれます。このように、データだけを持ちルールを持たないモデルは「ドメインモデル貧血症」と呼ばれます。

直した後:ルールを集約ルートに集める

record OrderLine(String product, Money unitPrice, int quantity) {
    Money subtotal() { return unitPrice.times(quantity); }
}
record OrderConfirmed(OrderId orderId, Money total) {}          // ドメインイベント

// 集約ルート:明細の追加・確定はすべてここを通す
class Order {
    private static final Money LIMIT = Money.yen(30_000);        // 1注文の上限
    private final OrderId id;
    private final List<OrderLine> lines = new ArrayList<>();
    private boolean confirmed = false;
    private final List<Object> events = new ArrayList<>();

    private Order(OrderId id) { this.id = id; }
    static Order create(OrderId id) { return new Order(id); }

    void addLine(String product, Money unitPrice, int quantity) {
        if (confirmed) throw new IllegalStateException("確定済みの注文は変更できません");
        var line = new OrderLine(product, unitPrice, quantity);
        if (total().add(line.subtotal()).isGreaterThan(LIMIT))
            throw new IllegalStateException("1注文の上限 " + LIMIT + " を超えます");
        lines.add(line);
    }
    void confirm() {
        if (lines.isEmpty()) throw new IllegalStateException("明細がありません");
        confirmed = true;
        events.add(new OrderConfirmed(id, total()));
    }
    Money total() { return lines.stream().map(OrderLine::subtotal).reduce(Money.yen(0), Money::add); }
    List<OrderLine> lines() { return List.copyOf(lines); }        // 読み取り専用のコピーを返す
    OrderId id() { return id; }
    List<Object> pullEvents() { var e = List.copyOf(events); events.clear(); return e; }
}

// リポジトリ:集約単位で保存・取得する(インターフェースはドメイン側に置く)
interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}

動かしてみます(リポジトリは、テスト用にメモリ上に保存する実装を使用)。

Order order = Order.create(new OrderId("ORD-1"));
order.addLine("ノート", Money.yen(300), 2);
order.addLine("ペン", Money.yen(120), 3);
System.out.println("合計=" + order.total() + ", 明細数=" + order.lines().size());

try { order.addLine("ファイル", Money.yen(5000), 10); }
catch (IllegalStateException e) { System.out.println("ルール違反: " + e.getMessage()); }

order.confirm();
repo.save(order);
System.out.println("発生したイベント: " + order.pullEvents());

Order loaded = repo.findById(new OrderId("ORD-1")).orElseThrow();
try { loaded.addLine("消しゴム", Money.yen(100), 1); }
catch (IllegalStateException e) { System.out.println("ルール違反: " + e.getMessage()); }

try { loaded.lines().add(new OrderLine("のり", Money.yen(150), 1)); }
catch (UnsupportedOperationException e) { System.out.println("外から明細は変更できない: " + e); }
合計=960 JPY, 明細数=2
ルール違反: 1注文の上限 30000 JPY を超えます
発生したイベント: [OrderConfirmed[orderId=OrderId[value=ORD-1], total=960 JPY]]
ルール違反: 確定済みの注文は変更できません
外から明細は変更できない: java.lang.UnsupportedOperationException

「確定後は変更不可」「上限を超えない」「合計は常に明細と一致」というルールが、Order の中だけで守られています。どの画面・どのAPIから使っても、ルールを破る方法がありません。

集約を設計するときの目安

  • 集約は小さく保つ。「一緒に変わらなければならないもの」だけを入れる
  • 他の集約は、オブジェクトではなくIDで参照する(Order は Member ではなく MemberId を持つ)
  • 1つのトランザクションでは、原則1つの集約だけを更新する。複数の集約にまたがる処理は、ドメインイベントなどでつなぐ

リポジトリ

リポジトリは、集約を保存・取得するための窓口です。インターフェースはドメイン層に置き、JPAやSQLを使った実装はインフラ層に置きます(依存性逆転の原則)。テーブルごとではなく、集約ごとに1つ作るのがポイントで、上の例では OrderLine 用のリポジトリは作りません。

ドメインサービス

どのエンティティや値オブジェクトに入れても不自然な業務ルールは、ドメインサービスに置きます。たとえば「2つの倉庫の間で在庫を移す」「会員IDが重複していないか確認する」などです。

ただし、何でもサービスに書くと、結局ルールがエンティティの外に散らばるため、まずはエンティティや値オブジェクトに置けないかを考え、それでも自然に置けないものだけをドメインサービスにします。

ドメインイベント

ドメインイベントは、「注文が確定した」のような、業務の専門家が関心を持つ出来事を表すオブジェクトです。名前は過去形にします。上の例の OrderConfirmed がこれにあたり、「確定したら確認メールを送る」「在庫を引き当てる」といった後続の処理を、注文の集約から切り離してつなげられます。なお、ドメインイベントはEvansの原著(2003年)の出版後に広まった考え方で、2015年の『Domain-Driven Design Reference』で主要なパターンとして整理されました。

レイヤー構成:ドメイン層を中心に置く

DDDでは、業務のルールを書くドメイン層を、技術的な詳細から独立させることを重視します。

層 役割 この記事の例
プレゼンテーション層 画面・APIの入出力 OrderController
アプリケーション層 ユースケースの手順を組み立てる(取得→ドメインの操作→保存)。業務ルールは書かない ConfirmOrderService
ドメイン層 業務のルールそのもの Order、Money、OrderRepository(インターフェース)
インフラ層 DB・外部APIなど技術的な実装 JpaOrderRepository

依存の向きは、外側から内側(ドメイン層)へそろえます。ドメイン層は、SpringやJPAを知らなくても動く状態が理想です(上の Order はフレームワークなしで動いています)。これを徹底した構成が、オニオンアーキテクチャ、ヘキサゴナルアーキテクチャ(ポートとアダプター)、クリーンアーキテクチャです。

よくある誤解

誤解 実際
DDD=レイヤードアーキテクチャ(やフォルダ構成) レイヤーはドメインを守るための手段の1つ。DDDの中心は、言葉と境界とモデル
DDD=値オブジェクトやリポジトリを使うこと 戦術的なパターンだけ真似る「軽量DDD」も役立つが、言葉と境界を決めないと効果は限られる
すべての領域にDDDを適用する 手間をかけるのはコアドメイン。単純な管理画面はシンプルな作りでよい
マイクロサービスにしないとDDDではない 1つのアプリケーションの中でも、モジュール(パッケージ)で境界を分ければ実践できる
最初に完璧なモデルを作る モデルは、業務の専門家との会話と実装をくり返して少しずつ深めていく
エンティティ=DBのテーブル エンティティは業務上の概念。1つの集約が複数のテーブルに保存されることもある

向いているプロジェクト・向いていないプロジェクト

向いている 向いていない(別の方法で十分)
業務のルールが複雑で、事業の強みに直結する 画面からDBへの登録・参照(CRUD)が中心
長期間、機能追加が続く 短期間で使い終わる、試作品
業務の専門家と継続的に話せる 業務の専門家に話を聞けない
複数のチームで、境界を分けて開発したい 1人・短期間の小さな開発

「DDDを採用するか、しないか」の二択ではなく、コアドメインの一部から取り入れることができます。

既存のプロジェクトで小さく始める手順

  1. 用語集を作る:業務の担当者との会話で出る言葉と、コードの名前の対応表を作る。ずれている名前を見つける
  2. 値オブジェクトを1つ導入する:金額、期間、数量など、チェックが各所に散らばっている値を record にする。効果が見えやすく、既存コードへの影響も小さい
  3. ルールを1つ、エンティティに移す:サービスにある「確定後は変更不可」のようなチェックを、order.confirm() のようなメソッドに移し、setterを減らす
  4. 集約の境界を決める:一緒に整合性を守るべき範囲を決め、外からは集約ルートのメソッドだけで変更するようにする
  5. リポジトリのインターフェースをドメイン側に置く:ドメインのテストを、DBなしで書けるようにする
  6. 境界づけられたコンテキストを見直す:同じ言葉が別の意味で使われている場所を見つけ、パッケージ(モジュール)を分ける

各ステップで、ドメインのテスト(DBもフレームワークも使わないテスト)を書くと、効果を確認しながら進められます。

学ぶための書籍

書籍 特徴
エリック・エヴァンス『エリック・エヴァンスのドメイン駆動設計』(今関剛 監訳、和智右桂・牧野祐子 訳、翔泳社) DDDの原典。戦略的設計まで含めた全体像
ヴォーン・ヴァーノン『実践ドメイン駆動設計』(髙木正弘 訳、翔泳社) 原典の考え方を実装にどう落とすかを詳しく解説
成瀬允宣『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』(翔泳社) 値オブジェクトなどの実装パターンから段階的に学べる入門書
Vlad Khononov『ドメイン駆動設計をはじめよう』(増田亨・綿引琢磨 訳、オライリー・ジャパン) 事業戦略と戦略的設計の結びつきをわかりやすく解説

入門の順番としては、実装パターンから入るなら成瀬さんの本、戦略的設計から入るならKhononovの本を読み、その後に原典に進むと理解しやすくなります。

よくある質問

Q. DDDはJavaでしか使えませんか? A. 言語は問いません。C#、Kotlin、TypeScript、Go、PHPなどでも実践されています。ただし、値オブジェクトを record で簡潔に書け、型でルールを表しやすいJavaやKotlinは、戦術的設計と相性が良い言語です。

Q. JPAのエンティティと、DDDのエンティティは同じにしてよいですか? A. 小規模なら、JPAのアノテーションをドメインのエンティティに直接付ける方法も現実的です。ただし、DBの都合(setterや引数なしコンストラクター)がドメインのルールを弱めやすいため、ルールが複雑な集約では、ドメインのモデルと永続化用のクラスを分け、リポジトリの実装で変換する方法がよく選ばれます。

Q. DDDとクリーンアーキテクチャの違いは何ですか? A. DDDは業務をどうモデル化するか(言葉・境界・モデル)の考え方、クリーンアーキテクチャはコードの依存関係をどう整理するかの考え方です。目的が近く、組み合わせて使われることが多いですが、別のものです。

Q. AIでコードを生成する時代に、DDDは役立ちますか? A. 役立ちます。ユビキタス言語の用語集や集約のルールを文章で渡すと、AIが生成するコードの名前や責任の分け方がそろいやすくなります。業務の言葉とコードが一致していると、生成されたコードのレビューもしやすくなります。

まとめ

  • DDDは、業務の知識と言葉を中心に置き、それをコードで表現する設計の考え方(Eric Evans、2003年)
  • まず戦略的設計:ユビキタス言語で言葉をそろえ、境界づけられたコンテキストでモデルを分ける
  • 次に戦術的設計:値オブジェクトで不正な値を作れなくし、集約でルールを1か所に集める
  • 集約は小さく、他の集約はIDで参照、1トランザクション1集約が目安
  • DDD=レイヤー構成ではない。レイヤーはドメインを守るための手段
  • 手間をかけるのはコアドメイン。CRUD中心の領域はシンプルでよい
  • 用語集と値オブジェクト1つから、小さく始められる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「ドメイン駆動設計入門|DDDの基本と小さく始める手順をJavaで解説」

株式会社RJCでは、業務を深く理解し、それを読みやすいコードで表現することを目指して、設計の考え方をプロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?