ストーリー
コンテキストマップとは
コンテキストマップは、Bounded Context間の関係を可視化した図です。
┌─────────────┐ ┌─────────────┐
│ 注文BC │ ─U/D─→ │ 決済BC │
│ (チームA) │ │ (チームB) │
└──────┬──────┘ └─────────────┘
│
│ Published
│ Language
▼
┌─────────────┐ ┌─────────────┐
│ 在庫BC │ ─ACL─→ │ 外部配送API │
│ (チームA) │ │ (外部企業) │
└─────────────┘ └─────────────┘
主要な関係パターン
1. Shared Kernel(共有カーネル)
2つのBCが共通のドメインモデルを共有します。
// 共有パッケージ
// shared-kernel/Money.ts
export class Money {
// 注文BCも決済BCも同じMoneyを使う
static of(amount: number, currency: string): Money { /* ... */ }
add(other: Money): Money { /* ... */ }
}
| 利点 | 注意点 |
|---|---|
| コードの重複を避けられる | 変更時に両チームの合意が必要 |
| 一貫性が保たれる | 結合度が高くなる |
2. Customer-Supplier(顧客-供給者)
上流(Supplier)が下流(Customer)にサービスを提供します。
┌─────────────┐ ┌─────────────┐
│ 注文BC │ ───────→ │ 配送BC │
│ (Supplier) │ │ (Customer) │
│ 上流 │ │ 下流 │
└─────────────┘ └─────────────┘
下流のニーズを上流が考慮してAPIを設計する
3. Conformist(順応者)
下流が上流のモデルにそのまま従います。交渉力がない場合のパターンです。
// 外部決済サービスのモデルにそのまま合わせる
class StripePaymentGateway implements PaymentGateway {
async charge(amount: Money, method: PaymentMethod): Promise<PaymentResult> {
// Stripeの仕様に合わせるしかない
const intent = await stripe.paymentIntents.create({
amount: amount.toCents(),
currency: amount.currency.toLowerCase(),
});
return this.toPaymentResult(intent);
}
}
4. Anti-Corruption Layer(腐敗防止層, ACL)
外部システムのモデルが自分のドメインを汚染しないように、変換層を設けます。
// ACL: 外部配送APIのモデルを自分のドメインモデルに変換
class ShippingApiAdapter implements ShippingService {
constructor(private externalApi: ExternalShippingApi) {}
async createShipment(order: Order): Promise<ShipmentId> {
// 外部APIの型 → 自ドメインの型 への変換
const externalRequest = {
// 外部APIは "recipient" という言葉を使う
recipient: {
name: order.shippingAddress.recipientName,
addr1: order.shippingAddress.line1,
addr2: order.shippingAddress.line2,
zip: order.shippingAddress.postalCode,
},
// 外部APIは "parcels" という単位
parcels: order.items.map(item => ({
sku: item.productId,
qty: item.quantity,
weight_g: item.weightGrams,
})),
};
const externalResponse = await this.externalApi.create(externalRequest);
// 外部APIの応答 → 自ドメインの型 への変換
return ShipmentId.fromString(externalResponse.tracking_number);
}
}
5. Published Language(公開言語)
Bounded Context間で共通のデータ形式を公開します。
// 注文BCが発行するイベント(Published Language)
// 他のBCはこの形式でイベントを受け取る
interface OrderCreatedEventPayload {
eventType: 'order.created';
version: '1.0';
data: {
orderId: string;
customerId: string;
items: Array<{
productId: string;
quantity: number;
unitPrice: number;
currency: string;
}>;
totalAmount: number;
currency: string;
createdAt: string; // ISO 8601
};
}
6. Open Host Service(公開ホストサービス)
標準的なプロトコルやAPIでサービスを公開します。
// REST APIとして公開(Open Host Service)
// GET /api/orders/:id
// POST /api/orders
// DELETE /api/orders/:id
// → OpenAPI/Swagger仕様で文書化
関係パターンの選択
| 状況 | 推奨パターン |
|---|---|
| 2チームが密接に協力 | Shared Kernel |
| 上流が下流のニーズを考慮 | Customer-Supplier |
| 外部サービスで交渉力なし | Conformist |
| 外部モデルの汚染を防ぎたい | Anti-Corruption Layer |
| 多くのBCにイベントを共有 | Published Language |
| APIとしてサービスを公開 | Open Host Service |
コンウェイの法則
「システムの構造は、それを設計した組織のコミュニケーション構造を反映する」
組織構造: システム構造:
┌─────┐ ┌─────┐ ┌─────────┐ ┌─────────┐
│TeamA│────│TeamB│ → │ 注文BC │────│ 決済BC │
└──┬──┘ └─────┘ └────┬────┘ └─────────┘
│ │
┌──▼──┐ ┌────▼────┐
│TeamA│ → │ 在庫BC │
│(兼任)│ │ │
└─────┘ └─────────┘
チーム境界とBounded Contextの境界を合わせることで、チーム間のコミュニケーションコストを最小化できます。
実践: コンテキストマップの描き方
1. Bounded Contextを四角形で描く
2. チーム名を記載する
3. 矢印で依存の方向を示す
4. 関係パターンのラベルを付ける
┌─────────────────┐
│ ECサイト │
│ │
│ ┌──────┐ U/D ┌──────┐
│ │注文BC│──────→│決済BC│
│ │TeamA │ │TeamB │
│ └──┬───┘ └──────┘
│ │ PL
│ │
│ ┌──▼───┐ ACL ┌──────┐
│ │在庫BC│──────→│配送API│
│ │TeamA │ │外部 │
│ └──────┘ └──────┘
│ │
└─────────────────┘
U/D = Upstream/Downstream
PL = Published Language
ACL = Anti-Corruption Layer
まとめ
| ポイント | 内容 |
|---|---|
| コンテキストマップ | BC間の関係を可視化する図 |
| 6つの関係パターン | SK, CS, CF, ACL, PL, OHS |
| ACL | 外部モデルの汚染防止層(最も重要) |
| コンウェイの法則 | 組織構造がシステム構造に反映される |
| チーム境界 | BCの境界とチーム境界を合わせる |
チェックリスト
- コンテキストマップの描き方を理解した
- 6つの関係パターンをそれぞれ説明できる
- ACLの必要性と実装方法を理解した
- コンウェイの法則とBCの関係を理解した
次のステップへ
次は演習です。ここまで学んだDDDの知識を使って、実際のドメインをモデリングしてみましょう。
推定読了時間: 40分