DDD を活用した ECサイトのドメインモデル設計 ～オーケストレーション／コレオグラフィ＋CQRS で集約を横断するデータ取得～

## 1. はじめに

Amazon のような大規模 EC サイトでは、購入フローに複数の機能 (Order, Payment, Inventory, Shipping など) が緊密に連携します。しかし、それらを1つの巨大モノリスにまとめると、**変更や保守が極めて困難**になりがちです。

この課題に対し、**DDD (ドメイン駆動設計)** ではドメインを「バウンデッドコンテキスト」に分割し、それぞれ**独立したモデル**を構築。さらに、**イベント駆動**で疎結合に連携することで、変更に強いアーキテクチャを目指します。

本記事では、以下のポイントを取り上げます。

1. **Order／Payment／Shipping／Inventory** それぞれの**集約(エンティティ)クラス図 (UML)**  
2. **エラーシナリオを含む状態遷移図**  
3. **オーケストレーション or コレオグラフィ**での通信方法の概略  
4. **CQRS を活用した読み取りモデル (Viewモデル)** による、**集約を横断するデータ取得**  
5. **シーケンス図にアプリケーションサービス層・メッセージブローカーを明示**し、集約間が直接通信しない点を表現

---

## 2. バウンデッドコンテキスト

### 2.1 大まかな役割分担

1. **Order Context**  
   - ユーザーから注文を受け、注文情報( Order, OrderLine )の状態を管理
2. **Payment Context**  
   - 決済を承認/却下する（外部決済サービスとの連携含む）
3. **Inventory Context**  
   - 在庫数を確保／引当を行い、不足があればバックオーダーやキャンセルを通知
4. **Shipping Context**  
   - 出荷準備や配送状況の更新、最終的な配送完了を管理

各コンテキストは**自律的にドメインモデル (Aggregate) を保持**し、**ドメインイベント**などを使って疎結合に協調します。

---

## 3. 集約ごとのクラス図 (UML)

### 3.1 Order Context のクラス図

```mermaid
classDiagram
    class Order {
        +id: string
        +status: OrderStatus
        +lines: OrderLine[]
        +confirm()
        +pay()
        +cancel()
        +ship()
        +calculateTotalAmount(): number
    }

    class OrderLine {
        +productId: string
        +quantity: number
        +unitPrice: number
        +getLineTotal(): number
    }

    class OrderStatus {
        <<enumeration>>
        Pending
        Confirmed
        Paid
        Shipped
        Canceled
        Backordered
    }

    Order "1" -- "1..*" OrderLine : has
```

- `Order.status` は `Pending` → `Confirmed` → `Paid` → `Shipped` → `Canceled` などを遷移  
- `Backordered` は**在庫不足**などで後から発送するケースを表す追加ステータス

### 3.2 Payment Context のクラス図

```mermaid
classDiagram
    class Payment {
        +id: string
        +orderId: string
        +amount: number
        +status: PaymentStatus
        +approve()
        +decline()
    }

    class PaymentStatus {
        <<enumeration>>
        Pending
        Approved
        Declined
    }
```

- `Payment` が集約ルートで、`orderId` と `amount` を保持。`status` が `Pending` → `Approved` / `Declined` に遷移  
- 外部決済APIやカード情報などは**ドメインサービスや外部アダプタ**で扱うイメージ

### 3.3 Shipping Context のクラス図

```mermaid
classDiagram
    class Shipping {
        +id: string
        +orderId: string
        +shippingStatus: ShippingStatus
        +address: Address
        +ship()
        +deliver()
        +fail()
    }

    class Address {
        +postalCode: string
        +city: string
        +detail: string
    }

    class ShippingStatus {
        <<enumeration>>
        Ready
        Shipped
        Delivered
        Failed
    }
```

- `Shipping` エンティティが `Ready` → `Shipped` → `Delivered` (or `Failed`) を管理  
- `fail()` は宛先不明、破損、受取拒否などで出荷に失敗した場合

### 3.4 Inventory Context のクラス図

```mermaid
classDiagram
    class Inventory {
        +id: string
        +productId: string
        +quantity: number
        +reserve(amount: number)
        +release(amount: number)
        +isAvailable(amount: number): boolean
    }
```

- 「特定商品IDの在庫数を管理する集約」を単純化した例  
- 実際には倉庫やロケーション単位など、より複雑な構造が必要なケースが多い

---

## 4. 状態遷移図：エラーシナリオも含む

### 4.1 Order の状態遷移例

```mermaid
stateDiagram-v2
    [*] --> Pending
    Pending --> Confirmed : confirm()
    Confirmed --> Paid : pay()
    Confirmed --> Canceled : cancel()

    Paid --> Shipped : ship()
    Paid --> Canceled : cancel()
    Shipped --> Delivered : (Optional step)
    Shipped --> [*]
    Canceled --> [*]

    note left of Confirmed: 在庫確保に失敗→<br/>Backordered へ遷移
    Confirmed --> Backordered : inventory shortage
    Backordered --> Paid : reserve succeeded?
    Backordered --> Canceled : cancel()
```

- `Confirmed` の段階で在庫不足なら `Backordered` へ遷移可能  
- `Backordered` から在庫入荷後に `Paid` へ移るシナリオなどを想定

### 4.2 Payment の状態遷移例 (エラー含む)

```mermaid
stateDiagram-v2
    [*] --> Pending
    Pending --> Approved : approve()
    Pending --> Declined : decline()
    Approved --> [*]
    Declined --> [*]
```

- `Declined` になったら `Order` 側が `Canceled` にするか、別カードで再決済するかなど、追加フローを検討

### 4.3 Shipping の状態遷移例

```mermaid
stateDiagram-v2
    [*] --> Ready
    Ready --> Shipped : ship()
    Shipped --> Delivered : deliver()
    Shipped --> Failed : fail()
    Delivered --> [*]
    Failed --> [*]
```

- `Failed` は宛先不明や受取拒否など  
- `Delivered` は最終到達を示す  

---

## 5. 集約間の通信方法：オーケストレーション or コレオグラフィ

### 5.1 オーケストレーション

- **指揮者 (OrderService / Sagaなど) が全体フローを制御**し、各サービスに「支払い依頼」「在庫引当依頼」「出荷依頼」などを順に指示  
- 成功／失敗の結果を受けて次のステップに進んだり、補償処理を実行

### 5.2 コレオグラフィ

- 各サービスが**ドメインイベント**を発行 (Publish) し、他サービスが購読 (Subscribe) して反応する形  
- **指揮者を置かず**、イベントの連鎖でフローが進む  
- 例：「OrderConfirmedEvent」が飛んできたら `PaymentService` が決済実行し、「PaymentApprovedEvent」を返す → それを `OrderService` が受け取って `order.pay()` など

実際の大規模システムでは、部分的に **オーケストレーション** を使いつつ一部は **コレオグラフィ** とするなど、ハイブリッドになることが多いです。

---

## 6. ドメインイベント連携シーケンス図：エラー想定＋サービス層の明示

以下は**コレオグラフィ的なイベント駆動**に近い例ですが、オーケストレーションでもアプリケーションサービス層を介する点は同じです。

```mermaid
sequenceDiagram
    participant User as User
    participant OrderService
    participant PaymentService
    participant InventoryService
    participant EventBus

    Note over User: ユーザーが購入ボタン押下
    User->>OrderService: confirmOrder()
    OrderService->>OrderAgg: order.confirm()
    OrderAgg-->>OrderService: (発行)OrderConfirmedEvent
    OrderService->>EventBus: publish(OrderConfirmedEvent)

    Note over PaymentService: OrderConfirmedEvent を購読
    EventBus-->>PaymentService: consume(OrderConfirmedEvent)
    PaymentService->>PaymentAgg: payment.create(Pending)
    PaymentService->>外部決済API: 決済実行 (approve or decline)

    alt 決済成功
        PaymentService->>PaymentAgg: payment.approve()
        PaymentService->>EventBus: publish(PaymentApprovedEvent)

        Note over OrderService: PaymentApprovedEvent を購読
        EventBus-->>OrderService: consume(PaymentApprovedEvent)
        OrderService->>OrderAgg: order.pay()

        OrderService->>InventoryService: 在庫引当依頼 (同期 or 非同期)
        alt 在庫不足
            InventoryService-->>OrderService: InventoryNotAvailableEvent
            OrderService->>OrderAgg: order.backorder()
        else 在庫OK
            InventoryService-->>OrderService: InventoryReservedEvent
            OrderService->>OrderAgg: proceed to shipping...
        end
    else 決済失敗
        PaymentService->>PaymentAgg: payment.decline()
        PaymentService->>EventBus: publish(PaymentDeclinedEvent)

        Note over OrderService: PaymentDeclinedEvent を購読
        EventBus-->>OrderService: consume(PaymentDeclinedEvent)
        OrderService->>OrderAgg: order.cancel()
    end
```

- **アプリケーションサービス層** (`OrderService`, `PaymentService`) と **EventBus** を明示  
- ドメインモデル（`OrderAgg` や `PaymentAgg`）間で直接呼び出すのではなく、**サービス層 or メッセージブローカー**を介する

---

## 7. CQRS による読み取りモデル（View）の活用

### 7.1 なぜ読み取りモデルが必要？

- DDD では各コンテキスト(BC)が独立した**ドメインモデル**を持つため、**横断的にJOIN**しないほうが独立性・変更容易性を保ちやすい  
- しかし、画面表示やレポートなどで「Order + Payment + Inventory + Shipping の状態を1画面に表示したい」など、**複数集約をまたぐデータ取得**のニーズがある  

そこで、**CQRS (Command Query Responsibility Segregation)** パターンを導入し、**書き込み (集約) と 読み取り (View) を分離**します。

- **ドメインイベント**を購読して**読み取り用データストア**に投影 (Projection) し、**複数の集約をまたぐ情報をまとめて保持**  
- フロントエンドや外部システムからの**読み込み要求**は、この**読み取りモデル (Viewモデル)** に対して行う。これにより、**疎結合かつスケーラブル**に横断的なデータを取得可能

### 7.2 アーキテクチャ図（マイクロサービス＋CQRS の読み取りモデル）

```mermaid
flowchart LR
    subgraph OrderMS
      OApp[OrderService]
      OCont[Order Domain Model]
      OInfra[Order Infra/Repo]
      OApp --> OCont
    end
    subgraph PaymentMS
      PApp[PaymentService]
      PCont[Payment Domain Model]
      PInfra[Payment Infra/Repo]
      PApp --> PCont
    end
    subgraph InventoryMS
      IApp[InventoryService]
      ICont[Inventory Domain Model]
      IInfra[Inventory Infra/Repo]
      IApp --> ICont
    end
    subgraph ShippingMS
      SApp[ShippingService]
      SCont[Shipping Domain Model]
      SInfra[Shipping Infra/Repo]
      SApp --> SCont
    end

    subgraph EventView
      QSvc[Query/Read Service]
      QDB[(Read DB)]
      QSvc --> QDB
    end

    OApp --Publish--> EventBus[(Message Broker)]
    PApp --Publish--> EventBus
    IApp --Publish--> EventBus
    SApp --Publish--> EventBus

    EventBus --OrderConfirmedEvent,etc.--> QSvc
    EventBus --PaymentApprovedEvent,etc.--> QSvc
    EventBus --ShippingDeliveredEvent,etc.--> QSvc
```

- **EventBus** (Kafka, RabbitMQ, etc.) が各マイクロサービスのアプリケーション層からの**ドメインイベント**を受信  
- **EventView** 側の `QSvc` がこれらのイベントを**購読**し、**横断的に結合した読み取り用DB**を更新  
- Webアプリや他システムは、**この Read DB** を参照して「Order + Payment + Shipping 状況」を一括で取得できる

---

## 8. エラー・リトライシナリオをより詳しく

- **決済失敗 (Declined)**  
  - `PaymentService` が `PaymentDeclinedEvent` をパブリッシュ → `OrderService` が購読して `order.cancel()`  
  - 場合によっては「別カードで再決済」するフローをアプリケーションサービス層で用意しておく  
- **在庫不足 (Backordered)**  
  - `InventoryService` が `InventoryNotAvailableEvent` をパブリッシュ → `OrderService` が購読して `order.backorder()`  
  - 後日、在庫が補充されたら `InventoryRestockedEvent` を出して、`OrderService` が `order.pay()` に進めるなど  
- **出荷失敗 (ShippingStatus = Failed)**  
  - `ShippingService` で `fail()` → `ShippingFailedEvent` → `OrderService` で `order.cancel()` or `order.reship()` といった補償パスを検討  

いずれのシナリオも、**アプリケーションサービス層 + ドメインイベント**を中心に状態遷移を進め、**CQRSの読み取りモデル**にも反映させることで、一貫性と拡張性を両立できます。

---

## 9. まとめ

1. **バウンデッドコンテキストごとに独立した集約**  
   - Order, Payment, Inventory, Shipping のように区切り、モノリス化を防ぎつつ、ドメインロジックを明確化  
2. **オーケストレーション or コレオグラフィ**  
   - 全体フローを1つのサービス (またはSaga) が制御するか、イベント駆動で自律的に進めるかを選択。大規模システムでは両者を組み合わせることも多い  
3. **状態遷移図＋エラーシナリオを明記**  
   - 決済失敗、在庫不足、出荷失敗など含めて「誰がどこでイベントを発行し、どの集約をどう更新するか」を整理  
4. **シーケンス図にアプリケーションサービス層やメッセージブローカーを明示**  
   - 集約同士が直接通信せず、疎結合を保つ。どのサービスがイベントをPublish/Subscribeして、どの集約メソッドを呼び出すかが可視化される  
5. **CQRS の読み取りモデル (View) を活用**  
   - 複数コンテキストのデータを横断的に参照するには、ドメインイベントを購読して集約間データを集約した**Read DB**を構築。高いスケーラビリティや可用性を確保できる

DDD は複雑なビジネスロジックを**ドメインモデル (Aggregate) 中心**に整理し、マイクロサービス＋イベント基盤と組み合わせることでスケーラブルかつ拡張性の高いアーキテクチャを実現します。さらに、**CQRS による読み取りモデル**を導入すれば、**集約を横断するデータ取得**も高パフォーマンスかつ疎結合で運用可能になります。ぜひ、UML やステートマシン図を用いて可視化しながら、チーム全体でシステムデザインを検討してみてください。

---

## 参考文献

- **エリック・エヴァンス『ドメイン駆動設計』** (DDDの青本)  
- **バーナード・ガワー『実践ドメイン駆動設計』**  
- **Vaughn Vernon 『Implementing Domain-Driven Design』**  
- **マーチン・ファウラー『Patterns of Enterprise Application Architecture』**  
- **Greg Young『CQRS Documents』**