ドメイン駆動設計(DDD)の基礎

はじめに

ドメイン駆動設計(Domain-Driven Design, DDD)は、エリック・エバンスによって2003年に提唱された、複雑なビジネスシステムを扱うためのソフトウェア開発手法です。その核心思想は、**ビジネスドメインをソフトウェア設計の中心に据え、精密なドメインモデルを構築することで、ビジネスの複雑性と技術的な複雑性を分離し、最終的にコードとビジネスを高度に一致させる**ことです。

DDDは主に二つの段階に分かれます:戦略的設計(システムをどう分割し、境界をどこに引くか)と戦術的設計(コードをどう組織し、モデルをどう構築するか)。以下に主要な用語と思想を詳しく解説します。

DDDの核心思想

1. 核心哲学

  • ビジネス優先:技術はビジネスに奉仕し、ビジネスを深く理解することがアーキテクチャ設計の前提です。
  • 統一言語:開発者、製品担当者、業務専門家が同一の用語を使い、コミュニケーションの壁を取り除きます。
  • モデル駆動:ドメインモデルを通じてビジネスルールを表現し、モデルがビジネスであり、コードがモデルです。
  • 分割統治:巨大なシステムを高凝集、低結合の小さなモジュール(境界づけられたコンテキスト)に分割します。

2. 二つの段階

  • 戦略的設計(Strategic Design)何をするか。ビジネス境界を分割し、システムアーキテクチャを確定し、チームの役割を明確にします。
  • 戦術的設計(Tactical Design)どうするか。境界内でドメインモデルを構築し、コード構造を設計し、ビジネスロジックを実装します。

戦略的設計の核心用語(マクロアーキテクチャ)

1. ドメイン (Domain)

定義:ソフトウェアが扱うべきビジネス問題の範囲と関連する知識の集合。

理解:例えば、eコマースドメイン、医療ドメイン、金融ドメインなど。すべての設計の出発点となります。

2. サブドメイン (Subdomain)

定義:ドメインの細分化されたモジュールで、一つのドメインは複数のサブドメインで構成されます。

分類(核心)

  • コアドメイン (Core Domain):ビジネスの競争力の核心で、重点的に投資し、自社開発する必要があります。(例:eコマースの取引システム
  • サポートドメイン (Supporting Domain):コアビジネスを支えますが、差別化の優位性ではありません。(例:eコマースの物流管理
  • 汎用ドメイン (Generic Domain):汎用的な機能で、ビジネス特性がなく、アウトソーシングやオープンソースを使用できます。(例:ユーザー認証、ログ

3. 境界づけられたコンテキスト (Bounded Context, BC)

定義DDDで最も重要な概念。モデルの意味が有効になる境界

役割

  • 各BC内に独立した統一言語独立したドメインモデルがあります。
  • 「一つの言葉に複数の意味」の問題を解決します。例えば、`商品`は商品コンテキストでは説明と価格ですが、物流コンテキストでは重量と容積を指します。

関係:通常、一つの境界づけられたコンテキストは一つのマイクロサービスまたは一つの開発チームに対応します。

4. コンテキストマッピング (Context Mapping)

定義境界づけられたコンテキスト間の協力関係と統合方法を記述します。

一般的なパターン

  • クライアント-サプライヤ (Customer-Supplier):下流が上流に依存し、上流が主導します。
  • アンチコラプションレイヤ (Anti-Corruption Layer, ACL):下流がアダプターレイヤーを通じて上流を隔離し、自身のモデルの純粋さを保護します。
  • 共有カーネル (Shared Kernel):二つのチームがモデルの小さな部分を共有します(慎重に使用し、結合が生じやすい)。
  • パブリッシュ-サブスクライブ (Publish-Subscribe)ドメインイベントを通じて通信を疎結合にします。

5. 統一言語 (Ubiquitous Language)

定義:チームが共同で作成したビジネス用語の辞書

要件:用語は一貫してコード、ドキュメント、日常会話に現れなければなりません。

価値:「ビジネスがビジネスを話し、開発者が開発する」というギャップを解消します。

戦術的設計の核心用語(ミクロモデリング)

1. エンティティ (Entity)

定義一意の識別子(ID)を持ち、属性は変化してもアイデンティティは不変のオブジェクト。

特徴:ライフサイクルがあり、状態があり、追跡が必要です。

:`ユーザー(User)`、`予約(Reservation)`。IDは不変ですが、予約のステータスは「未支払い」から「完了」に変わる可能性があります。

2. 値オブジェクト (Value Object)

定義一意のIDを持たず属性値によって定義されるオブジェクト。

特徴不変で、ライフサイクルがなく、置き換え可能

:`住所(Address)`、`金額(Money)`、`色(Color)`。

原則:エンティティの特徴を記述しますが、それ自体に独立した存在意義はありません。

3. 集約 (Aggregate) & 集約ルート (Aggregate Root)

定義:一連の密接に関連するエンティティと値オブジェクトの集合で、データの変更と永続化の基本単位です。

集約ルート (AR):集約のエントリーポイント/管理者であり、特別なエンティティです。

ルール(核心)

  1. 外部は集約ルートを通じてのみ集約内部のオブジェクトにアクセスできます。
  2. トランザクション境界:一つのトランザクションは一つの集約のみを変更できます。
  3. 一貫性:集約ルートは内部データのビジネス的一貫性を維持します。

:`予約(Reservation)`が集約ルートで、`予約明細(ReservationItem)`、`配送先住所(Address)`を含みます。外部は`Reservation`のメソッドを呼び出すだけで、`ReservationItem`を直接変更することはできません。

4. ドメインサービス (Domain Service)

定義:単一のエンティティ/値オブジェクトに置く適切でないステートレスなビジネスロジックをカプセル化します。

シナリオ:複数のエンティティ、複数の集約にまたがるビジネスロジック。

:`支払いサービス(PaymentService)`、`価格計算サービス(PricingService)`。

原則:純粋なビジネスロジックのみを行い、IO、トランザクション、権限処理は行いません。

5. ドメインイベント (Domain Event)

定義:ドメイン内で発生したビジネス上有意義な状態変更

役割疎結合。一つの集約がイベントを発行し、他の集約/コンテキストが非同期にサブスクライブして処理します。

:`予約が支払われた(ReservationPaidEvent)` → 在庫減少、物流出荷をトリガーします。

利点:最終一貫性、高い拡張性。

6. リポジトリ (Repository)

定義集約ルートのコンテナで、永続化(CRUD)機能を提供します。

役割ドメイン層とデータベースを分離します。ドメイン層はRepositoryのインターフェースにのみ依存し、具体的な実装(MySQL/Mongo)はインフラストラクチャ層にあります。

原則:一つの集約ルートに一つのRepositoryが対応します。

7. ファクトリ (Factory)

定義複雑なオブジェクト(集約)の作成ロジックをカプセル化します。

役割:作成の詳細を隠蔽し、オブジェクトが作成された時点で完全かつ有効であることを保証します。

DDDの標準的な階層アーキテクチャ

DDDは階層を通じて関心事を分離し、ドメイン層は絶対的に独立しており、どのフレームワークや技術的な詳細にも依存しません。

1. ユーザーインターフェース層 (Interfaces)

責任:外部リクエスト(HTTP/RPC)を処理し、パラメータを検証し、DTOを返します。

コンポーネント:Controller、Facade、DTOコンバーター。

2. アプリケーション層 (Application)

責任非常に薄い。**編成のみを行い、ビジネスを実装しない**。

仕事:ドメインサービス/集約ルートを呼び出し、トランザクション制御、権限検証、ドメインイベントの送信を行います。

コンポーネント:XXXApplicationService。

3. ドメイン層 (Domain) —— 核心層

責任すべてのビジネス概念、ルール、状態を表現します。

コンポーネント:Entity、VO、Aggregate、Domain Service、Domain Event、Repositoryインターフェース。

原則純粋なビジネスで、技術的なコード(Spring、JDBCなど)は一切含みません。

4. インフラストラクチャ層 (Infrastructure)

責任技術的なサポートを提供します。

コンポーネント:Repositoryの実装(MyBatis/JPA)、メッセージキュー、ログ、キャッシュ、サードパーティSDK。

実践例:予約システムのDDDモデリング

「ホテル予約業務」を用いて、完全なDDDモデリングの流れを体験します。

イベントストーミング → ドメインの分割 → 境界づけられたコンテキスト → 集約の設計 → コード構造 → 完全なビジネスフロー

1. ビジネスシナリオ(何をやるかを明確にする)

簡略化されたホテル予約フロー:

  1. ユーザーが予約を作成(部屋、チェックイン日、支払い方法を選択)
  2. システムが在庫、価格、クーポンを検証
  3. 予約が作成され、未支払い状態になる
  4. ユーザーが支払いを行い、予約が支払い済みになる
  5. 部屋の在庫を減らし、宿泊施設に通知を送信

このフローに基づいて完全なDDDモデリングを行います。

2. 第一步:イベントストーミング(DDDの核心的な分割方法)

イベントストーミングの目的は一つだけ:**ビジネスをきれいに分割し、ドメイン、サブドメイン、境界づけられたコンテキストを見つける**

1. すべてのドメインイベントを特定する

ドメインイベント = ビジネス上で**有意義なことが発生した**こと

まずすべてをリストアップします:

  • ReservationCreatedEvent(予約が作成された)
  • ReservationSubmittedEvent(予約が提出された)
  • ReservationPaidEvent(予約が支払われた)
  • ReservationCancelledEvent(予約がキャンセルされた)
  • RoomInventoryDeductedEvent(部屋在庫が減らされた)
  • NotificationSentEvent(通知が送信された)

2. イベントをトリガーするコマンドを見つける

  • CreateReservationCommand
  • SubmitReservationCommand
  • PayReservationCommand
  • CancelReservationCommand

3. 参加するエンティティを見つける

  • User(ユーザー)
  • Reservation(予約)
  • ReservationItem(予約明細)
  • Room(部屋)
  • Inventory(在庫)
  • Notification(通知)
  • Payment(支払い)

4. 集約のグループ化(重要なステップ)

ビジネスの内聚性に基づいてグループ化します:

集約 1:予約集約
  • Reservation(集約ルート)
  • ReservationItem
  • Address(値オブジェクト)
  • ReservationAmount(値オブジェクト)
集約 2:部屋在庫集約
  • Inventory(集約ルート)
  • Room
集約 3:支払い集約
  • Payment(集約ルート)
集約 4:通知集約
  • Notification(集約ルート)

3. 第二步:戦略的設計——ドメインと境界づけられたコンテキストの分割

1. ドメイン Domain

ホテル予約ビジネスドメイン

2. サブドメイン Subdomain

  • コアドメイン:予約ドメイン(競争力の核心)
  • サポートドメイン:部屋在庫ドメイン、通知ドメイン
  • 汎用ドメイン:ユーザードメイン、支払いドメイン(サードパーティを接続可能)

3. 境界づけられたコンテキスト Bounded Context

最終的に4つのBC(マイクロサービスに対応)に分割します:

  1. ユーザーコンテキスト(User BC)
  2. 予約コンテキスト(Reservation BC) —— 核心
  3. 部屋在庫コンテキスト(Room BC)
  4. 支払いコンテキスト(Payment BC)
  5. 通知コンテキスト(Notification BC)

一つの境界づけられたコンテキスト ≈ 一つのマイクロサービス
内部に独自のモデル、言語、データベースがあります

4. 第三步:戦術的設計——予約集約モデル(最も核心)

1. 予約集約の構造

// 集約ルート
public class Reservation {
    private ReservationId id;
    private UserId userId;
    private Address address;
    private ReservationStatus status;
    private ReservationAmount totalAmount;
    private List<ReservationItem> items;

    // 予約の提出
    public void submit() {
        if (this.status != ReservationStatus.DRAFT) {
            throw new IllegalStateException("草稿状態の予約のみ提出できます");
        }
        this.status = ReservationStatus.PENDING_PAY;
        // ドメインイベントの発行
        DomainEventPublisher.publish(new ReservationSubmittedEvent(id));
    }

    // 予約の支払い
    public void pay() {
        if (this.status != ReservationStatus.PENDING_PAY) {
            throw new IllegalStateException("予約の状態が不正です");
        }
        this.status = ReservationStatus.PAID;
        DomainEventPublisher.publish(new ReservationPaidEvent(id, userId));
    }
}

// 予約明細(子エンティティ)
public class ReservationItem {
    private RoomId roomId;
    private Money price;
    private int nights;
}

// 住所(値オブジェクト)
public class Address {
    private String street;
    private String city;
    private String zipCode;
}

// 予約金額(値オブジェクト)
public class ReservationAmount {
    private Money subtotal;
    private Money tax;
    private Money total;
}

2. エンティティ vs 値オブジェクトの明確な区別

  • エンティティ:IDがあり、変化し、ライフサイクルがある
    Reservation、ReservationItem、Payment
  • 値オブジェクト:IDがなく、不変で、記述のみ
    Address、Money、Coordinate、ReservationAmount

3. 集約ルートのルール(非常に重要)

  1. 外部は集約ルートを通じてのみ集約内部のオブジェクトにアクセスでき、直接操作してはいけません。
  2. 一つのトランザクションは一つの集約のみを変更できます。
  3. 集約ルートは内部データのビジネス的一貫性を保証します。
    例:支払い済みの予約は数量を変更できません。

5. 第四步:DDD四層アーキテクチャ + コード構造

標準的なDDDのパッケージ構造(Javaスタイル、すべての言語に適用可能)

reservation/
├─ interfaces/        # インターフェース層:Controller、DTO
├─ application/        # アプリケーション層:ビジネスの編成
├─ domain/            # ドメイン層(核心)
│   ├─ entity/        # エンティティ、集約ルート
│   ├─ vo/            # 値オブジェクト
│   ├─ event/         # ドメインイベント
│   ├─ service/       # ドメインサービス
│   └─ repository/    # Repositoryインターフェース
└─ infrastructure/    # インフラストラクチャ層
    └─ repository/impl # Repositoryの実装(MyBatis/JPA)

6. 第五步:コードの実戦(最も重要なビジネスロジック)

予約の作成 → 提出 → 支払いの実際のDDDスタイルのロジックを直接記述します。

1. ドメイン層:予約集約ルート(核心ビジネスルールはここにあります)

// 集約ルート
public class Reservation {
    private ReservationId id;
    private UserId userId;
    private Address address;
    private ReservationStatus status;
    private ReservationAmount totalAmount;
    private List<ReservationItem> items;

    // 予約の提出
    public void submit() {
        if (this.status != ReservationStatus.DRAFT) {
            throw new IllegalStateException("草稿状態の予約のみ提出できます");
        }
        this.status = ReservationStatus.PENDING_PAY;
        // ドメインイベントの発行
        DomainEventPublisher.publish(new ReservationSubmittedEvent(this.id));
    }

    // 予約の支払い
    public void pay() {
        if (this.status != ReservationStatus.PENDING_PAY) {
            throw new IllegalStateException("予約の状態が不正です");
        }
        this.status = ReservationStatus.PAID;
        DomainEventPublisher.publish(new ReservationPaidEvent(this.id, this.userId));
    }
}

ポイント:
**すべてのビジネスルール、検証、ステータス変更はドメインクラスの内部で行われます**
Serviceでビジネスロジックを適当に書かないでください。

2. ドメインイベント(マイクロサービス間の疎結合の神器)

public class ReservationPaidEvent {
    private ReservationId reservationId;
    private UserId userId;
}

予約が支払われた後、イベントを発行します:

  • 在庫サービスがリッスン → 在庫を減らす
  • 通知サービスがリッスン → 通知を送信

完全に疎結合で、高凝集、低結合

3. ドメインサービス(複数集約のロジック)

public class ReservationDomainService {

    // 予約を作成し、同時に価格と在庫を検証
    public Reservation createReservation(User user, List<Room> rooms, Address address) {
        // ロジックが複数の集約にまたがるため、DomainServiceに置かれます
        Reservation reservation = ReservationFactory.create(user, rooms, address);
        return reservation;
    }
}

4. アプリケーション層(編成のみを行い、ビジネスを実装しない)

@Service
public class ReservationApplicationService {

    private ReservationRepository reservationRepository;
    private ReservationDomainService reservationDomainService;

    // 予約の作成
    @Transactional
    public ReservationDto createReservation(CreateReservationRequest request) {
        User user = userRepository.get(request.getUserId());
        List<Room> rooms = roomRepository.list(request.getRoomIds());

        Reservation reservation = reservationDomainService.createReservation(user, rooms, request.getAddress());
        reservationRepository.save(reservation);

        return ReservationConverter.toDto(reservation);
    }

    // 予約の支払い
    @Transactional
    public void payReservation(PayReservationRequest request) {
        Reservation reservation = reservationRepository.get(request.getReservationId());
        reservation.pay();          // ドメインの振る舞い
        reservationRepository.save(reservation);
    }
}

5. インターフェース層(Controller)

@RestController
@RequestMapping("/reservation")
public class ReservationController {

    private ReservationApplicationService reservationAppService;

    @PostMapping("/create")
    public Result<ReservationDto> create(@RequestBody CreateReservationDto dto) {
        CreateReservationRequest request = RequestConverter.toRequest(dto);
        return Result.success(reservationAppService.createReservation(request));
    }
}

6. リポジトリインターフェース(ドメインはインターフェースにのみ依存し、実装には依存しない)

public interface ReservationRepository {
    Reservation get(ReservationId id);
    void save(Reservation reservation);
}

実装クラスはインフラストラクチャ層にあり、MyBatis/JPAどちらでも使用できます。

7. 第六步:完全なビジネスフロー(DDDの視点で実行)

  1. フロントエンドリクエスト → Controller
  2. ControllerがRequestに変換 → アプリケーションサービス
  3. アプリケーションサービス
    • 集約をロード
    • 集約ルートのメソッドを呼び出し(真のビジネスロジック)
    • 集約を保存
    • トランザクションをコミット
  4. 集約内部でドメインイベントを発行
  5. 他のマイクロサービスがイベントをリッスンして自身のロジックを実行

システム全体が以下を実現します:

  • ビジネスロジックの高凝集
  • 状態の安全な制御
  • 高い拡張性、ロジックを追加する場合はイベントを追加するだけ
  • マイクロサービス間の高度な疎結合

タグ: ドメイン駆動設計 DDD アーキテクチャ マイクロサービス 戦略的設計

6月18日 20:57 投稿