マイクロサービスにおけるドメイン駆動設計 (DDD)
組織がモノリシック アーキテクチャからマイクロサービスに移行するとき、サービスの境界線をどのように引くかという、一か八かの重大な問題に直面します。
理論的には、マイクロサービスは、独立して開発、デプロイ、スケーリングできる、緩やかな分離されたユニットである必要があります。しかし、実際には、多くのチームが 分散モノリス を構築することになります。これは、サービスが非常に密接に結合されているシステムであり、1 つのビジネス変更で複数のサービスを同時に変更および展開する必要があり、ネットワーク遅延と展開の行き詰まりが悪化します。
この落とし穴を避けるために、ソフトウェア アーキテクトは ドメイン駆動設計 (DDD) に注目します。 2003 年に Eric Evans によって初めて導入された DDD は、コード構造をそれが表す複雑なビジネス ドメインに適合させるソフトウェア開発方法論です。
この記事では、クリーンで分離された、保守性の高いマイクロサービスを設計するための戦略的および戦術的な青写真を DDD がどのように提供するかを見ていきます。
1. 戦略的青写真: サービスの境界線を引く
DDD は、戦略設計 と 戦術設計 という 2 つの主なフェーズに分かれています。戦略的設計とは、ビジネス コンテキストをモデル化し、サービスの境界を定義することです。アーキテクチャの「マクロ」ビューを提供します。
A. ユビキタス言語
企業内では、異なる部門が同じ言葉をまったく異なる意味で使用します。たとえば:
- 営業チームにとって、「ユーザー」は見込み客または潜在的な顧客です。
- セキュリティ チームにとって、「ユーザー」はログイン資格情報のセットです。
- 配送チームにとって、「ユーザー」は物理的な受信者の住所です。
すべての人を満足させる「ユーザー」向けの単一の統合データベース モデルを構築しようとすると、大規模で複雑なコードベースが作成されます。 DDD は、ユビキタス言語 (特定の境界内でビジネス ドメインの専門家と開発者の両方によって定義および使用される共有語彙) を確立することでこの問題を解決します。
B. 境界のあるコンテキスト
境界コンテキスト は、ドメイン モデルが適用される明示的な境界です。境界内では、ユビキタス言語のすべての用語は単一の明確な意味を持ちます。
単一の「ユーザー」モデルの代わりに、それぞれの境界付きコンテキスト内で個別のモデルを定義します。
- 注文コンテキストには、顧客の連絡先情報を含む
Orderモデルがあります。 - アイデンティティ コンテキストには、
Credentialsモデルがあります。 - ** Shipping Context** には、
DeliveryAddressモデルがあります。
マイクロサービスの黄金律: 1 つの境界付きコンテキストが 1 つのマイクロサービスに直接マッピングされます。
システムを境界コンテキストに分割することで、マイクロサービスが疎結合になり、異なるビジネス機能を表現できるようになります。
C. コンテキスト マッピング: サービスの通信方法
境界付きコンテキストは孤立して存在するわけではありません。彼らは対話しなければなりません。 コンテキスト マップ は、コンテキスト間の関係と変換メカニズムを定義します。主なパターンは次のとおりです。
- 共有カーネル: 2 つのコンテキストは、ドメイン モデルとデータベースの小さなサブセットを共有します (通常、結合のためマイクロサービスでは推奨されません)。
- 顧客-サプライヤー: 1 つのコンテキスト (サプライヤー) は別のコンテキスト (顧客) にデータを提供する必要があります。サプライヤーは、顧客とリリース スケジュールを調整する必要があります。
- 腐敗防止層 (ACL): 外部システムから受信したデータをクライアントの内部ドメイン モデルに変換し、外部モデルによる内部アーキテクチャの汚染を防ぐ変換層。
2. 戦術的な青写真: マイクロサービス コードベースの構造化
戦略的設計を使用してサービス境界が描画されると、戦術的設計 は、単一のマイクロサービス内のコードを構造化するための一連の設計パターンを提供します。
A. エンティティと値オブジェクト
マイクロサービス内では、モデルは次の 2 つのカテゴリに分類されます。
- エンティティ: 属性が変化しても、時間の経過とともに持続する一意のアイデンティティを持つオブジェクト。例には、
OrderまたはProductが含まれます。 - 値オブジェクト: 一意のアイデンティティを持たず、完全に属性によって定義されるオブジェクト。それらは不変です。例には、
ShippingAddressまたはProductDimensionsが含まれます。 2 つの値オブジェクトが同じ属性を持つ場合、それらは等しいとみなされます。
B. 集約と集約ルート
集計 は、データ変更の単一単位として扱われる、関連するエンティティと値オブジェクトのクラスターです。 すべての集約には 集約ルート があり、これが外部オブジェクトが集約と対話できる唯一のエントリ ポイントです。ルートは、すべてのビジネス不変条件 (ルール) が確実に適用されるようにします。
たとえば、上の図では次のようになります。
- Order Service では、
Orderが集約ルートです。これには、OrderItem(エンティティ) とShippingAddress(値オブジェクト) が含まれます。外部サービスはOrderItemを直接変更できません。Orderルート (order.AddItem()など) でメソッドを呼び出す必要があります。これにより、注文がまだ出荷されていないことが検証されます。
C. ドメインイベント
ドメイン イベント は、ビジネス エキスパートが関心を持つドメイン内で発生した出来事です。これは、境界付きコンテキスト間で変更を非同期に伝達するために使用されます。
集約がコマンドを実行すると、ドメイン イベント (OrderCreated など) が発行されます。他のサービスはこのイベントをリッスンし、それに応じて状態を更新し、同期 API 依存関係のない最終的な整合性を確保します。
3. 具体的な例: 注文サービスと在庫サービス
オーダー サービスに対する Go での戦術的 DDD の簡略化された実装を見て、集約ルートがビジネス ルールをどのように調整するかを示します。
package domain
import (
"errors"
"time"
)
// Value Object (Immutable, identity-less)
type ShippingAddress struct {
Street string
City string
ZipCode string
}
// Entity (Has identity, mutable)
type OrderItem struct {
ProductID string
Quantity int
Price float64
}
// Aggregate Root (Enforces transactional boundaries)
type Order struct {
ID string
Items []OrderItem
Address ShippingAddress
Status string
CreatedAt time.Time
}
// NewOrder creates a new Order Aggregate Root
func NewOrder(id string, address ShippingAddress) *Order {
return &Order{
ID: id,
Items: []OrderItem{},
Address: address,
Status: "PENDING",
CreatedAt: time.Now(),
}
}
// AddItem enforces business rules before mutating state
func (o *Order) AddItem(productID string, qty int, price float64) error {
if o.Status != "PENDING" {
return errors.New("cannot add items to a finalized or cancelled order")
}
if qty <= 0 {
return errors.New("quantity must be greater than zero")
}
o.Items = append(o.Items, OrderItem{
ProductID: productID,
Quantity: qty,
Price: price,
})
return nil
}
注文が正常に保存されると、サービスはイベントをメッセージ ブローカーに発行します。
{
"event_id": "evt_98231",
"event_type": "OrderCreated",
"timestamp": "2026-06-26T00:15:00Z",
"payload": {
"order_id": "ord_5521",
"items": [
{ "product_id": "prod_88", "quantity": 2 }
]
}
}
Inventory Service はこのイベントをリッスンし、ローカルの StockLevel エンティティを更新し、フローを非同期で完了します。
4. マイクロサービスにおける戦略的 DDD と戦術的 DDD
| フェーズ / アスペクト | 戦略的 DDD | 戦術的なDDD |
|---|---|---|
| 範囲 | グローバル システム アーキテクチャ (マクロ) | 単一サービス コードベース内 (マイクロ) |
| 主な目標 | 明確で分離されたサービス境界を描画する | 豊富なビジネス ロジックをモデル化し、不変条件を適用する |
| 主要な概念 | 境界コンテキスト、ユビキタス言語、コンテキストマップ | エンティティ、値オブジェクト、集計、ドメイン イベント |
| 視聴者 | アーキテクト、プロダクト マネージャー、開発者 | ソフトウェア開発者、コードレビュー担当者 |
| マイクロサービスへの影響 | サービスの数と範囲を決定します。データベース トランザクションとディレクトリ構造を決定します。 |
結論: まずは戦略的 DDD から始める
ドメイン駆動設計をマイクロサービスに適用することは、アーキテクチャがビジネス構造と確実に一致するようにする強力な方法です。ただし、チームは戦略的な設計を無視して、戦術的なパターン (リポジトリ インターフェイスや値オブジェクトの作成など) に重点を置きすぎるという間違いを犯すことがよくあります。
境界を正しく把握していなければ、いくらきれいな戦術コードを用意しても、システムが分散モノリスになるのを防ぐことはできません。常に戦略的なマッピングから始めてください。ユビキタス言語を定義し、ロジックを境界付きコンテキストにグループ化し、通信フローをマッピングして、それらの定義からマイクロサービスの境界を自然に浮かび上がらせます。
Ghaznix ブログでソフトウェア アーキテクチャ、設計パターン、エンジニアリングに関する洞察をさらに詳しくご覧ください →