プログラミング
インテグレーションイベントによる境界の越境
Crossing Boundaries with Integration Events (deniskyashif.com)
要約
この記事は、ドメイン駆動設計(DDD)におけるインテグレーションイベントの重要性について解説しています。異なる境界を持つコンテキスト間で情報をやり取りする際に、ドメインイベントをそのまま公開するのではなく、外部向けに設計されたインテグレーションイベントとして定義することの利点を説明しています。これにより、コンテキスト間の結合度を低く保ち、各コンテキストの独立性を維持しながら、信頼性の高い非同期通信を実現する方法を探ります。
全文翻訳
2026年8月29日 | 編集 ソフトウェアアーキテクチャ ドメイン駆動設計 分散システム インテグレーションイベントによる境界の越境
この記事は、ドメイン駆動設計(DDD)に関する継続シリーズの一部です。シリーズの他の記事はこちらで確認できます。この記事は、ある境界付けられたコンテキストが別のコンテキストに何かが発生したことを伝えるために、すべてが正しく行われる必要があること、そしてそれがうまくいかない場合の設計方法について説明します。
「モデリングの事実と反応をドメインイベントで」で見たように、ドメインイベントは、その事実が真実になったモデル内で意味のあるビジネス上の事実を記録します。これはローカルな反応を引き起こす可能性がありますが、その名前、型、ペイロードはドメインモデルに属し、モデルと共に自由に進化できます。ドメインイベントは内部イベントと考えることができます。ここでは、事実が別の境界付けられたコンテキスト、つまり独自の言語と責任を持つモデルにまたがる場合に何が起こるかを見ていきます。
境界の越境
注文管理(Ordering)と履行(Fulfillment)が独立して所有・デプロイされているコンテキストであり、履行側が新たに配置された注文を準備する必要があるとします。両者は、注文管理の内部モデルに結合させることなく、履行が必要とするビジネス情報を含む安定した公開契約(パブリックコントラクト)を必要とします。この契約がインテグレーションイベントであり、外部イベントと考えることができます。
インテグレーションイベントは、Apache Kafka、RabbitMQ、Azure Service Busなどのインフラストラクチャを使用して、耐久性のある非同期メッセージングを介して伝送されるため、注文管理は履行を待たずに完了できます。各コンテキストは独自のペースで作業を処理でき、一時的な障害がプロデューサーに逆伝播する必要はなく、コンシューマーが処理する準備ができるまでメッセージは利用可能なままになります。
これらの利点には新たな課題が伴います。どちらのコンテキストも利用できない可能性があり、メッセージが遅延したり重複したりする可能性があり、2つのコンテキストはデータベーストランザクションを共有できません。したがって、解決すべき2つの問題があります。
契約の設計
信頼性の高い配信
境界のための契約設計
ドメインモデルを公開しないでください
このモデルで、注文を配置すると履行可能になり、その時点で注文管理がこのドメインイベントを発行すると仮定します。
OrderPlaced
orderId: OrderId
customer: CustomerId
items: List<OrderItem>
placedAt: Timestamp
OrderItem
productId: ProductId
quantity: Quantity
このイベントは、注文管理の内部ハンドラ向けに設計されています。ProductId、Quantity、その他の型指定された識別子は、注文管理のモデルに属する値オブジェクトであり、モデルと共に自由に変化できます。イベントを直接公開すると、これらの内部情報が公開契約(パブリックコントラクト)になります。これはイベント駆動型統合で最もよくある間違いでしょう。再利用のように感じられます。イベントはすでに存在するので、なぜ共有しないのか?しかし、今やすべてのコンシューマーは注文管理のモデルに依存しており、注文管理自身のニーズのために行われたリファクタリングは、他のチームを壊し始めます。
ペイロードも境界には不向きです。顧客情報(Fulfillmentが不要とする)を公開し、注文管理の内部識別子を使用して製品を記述しています。
代わりに、注文管理は同じ事実を、履行コンテキスト向けに設計されたインテグレーションイベントに変換できます。
{
"metadata": {
"type": "order-ready-for-fulfillment",
"version": 1,
"messageId": "01JZ2Q5Y7M8K9N0P1R2S3T4V5W",
"occurredAt": "2026-08-29T09:42:18Z",
"orderEventSequence": 4
},
"body": {
"orderId": "ord-8472",
"items": [
{ "productCode": "CHAIR-BLK", "quantity": 2 },
{ "productCode": "DESK-OAK", "quantity": 1 }
]
}
}
このエンベロープは、トランスポートと契約のメタデータをビジネスペイロードから分離します。メタデータはイベント契約を識別し、メッセージのルーティング、重複排除、解釈に使用される情報を含みます。ボディは、履行によって消費される事実を含みます。orderEventSequenceは、注文IDごとのプロデューサー割り当てカウンターです。後でその有用性がわかります。
正確なJSON形式は必須ではありません。一部のメッセージングプラットフォームはメタデータをヘッダーに配置し、CloudEventsのような標準は独自のエンベロープフィールドを定義しています。重要なのは、分離が意味論的であり、プロデューサーとコンシューマー間で一貫していることです。均一なエンベロープにより、メッセージングインフラストラクチャは、ボディを理解することなく、トレーシングや重複排除などの懸念事項を処理できます。
OrderPlacedドメインイベントとこのインテグレーションイベントは、名前や形状を共有せずに同じ発生を記述しています。インテグレーションイベントは、注文管理の値オブジェクトクラスではなく、安定したシリアル化された値を含み、履行が不要とする顧客情報を削除しています。
注文管理はこの境界固有の契約を所有しており、別の境界は同じドメインイベントから派生した異なるインテグレーションイベントを必要とする場合があります。ペイロードをコンシューマーが必要とする事実に限定することで、結合度が制限されます。プロデューサー側のリファクタリングが波及する可能性は、公開するプロデューサー内部の詳細が少ないほど低くなります。コンシューマーが取得できるようにするのではなく、どれだけのデータを運ぶかについては、それ自体がトレードオフであり、以下で説明します。
インテグレーションイベントは公開インターフェースです。別のコンテキストがそれに依存すると、変更はバージョン管理されたRESTまたはgRPC APIの変更と同じ互換性の懸念を伴います。そのスキーマ、フィールドの意味、保証、およびバージョニングポリシーは、そのインターフェースの一部となります。
内部状態変更ではなくビジネス上の事実
名前は、ペイロードと同様に、内部ドメインモデルを漏洩させる可能性があります。履行側が注文管理のために次のイベントを発行すると仮定します。
FulfillmentStatusChanged
orderId: string
oldStatus: PICKING // あるいはさらに悪いことに、文字列の代わりに数値キー
newStatus: DISPATCHED
この契約により、注文管理は履行のステータスを理解し、そのステートマシンの部分を再現する必要があります。注文管理が気にするのはごくわずかであっても、すべての遷移を公開する可能性があります。ステータスの変更は、ビジネス上の事実が変わっていない場合でも、すべてのコンシューマーに影響を与える可能性があります。
この契約は、他のコンテキストを履行の内部モデルに結合させ、実装の詳細を漏洩させます。非同期メッセージングは、それ自体では結合を削除しません。契約は依然としてビジネス上の意味を表現する必要があります。同じやり取りは、注文管理にとって重要な事実として表現できます。
OrderDispatched
orderId: string
dispatchedAt: Timestamp
OrderDispatchedは、注文管理が履行の内部ライフサイクルを解釈する必要なしに、何が起こったかを伝えます。FulfillmentStatusChangedは、ステータス変更自体が意味のあるドメイン言語である場合には、本質的に間違っているわけではありません。フィールドを同期したり、実装の詳細を公開したりするだけの場合に問題となります。
データイベントはインテグレーション契約ではない
イベントソーシングと変更データキャプチャ(CDC)は、独自のイベントストリームを生成し、それらをすぐに使えるインテグレーションイベントとして扱う誘惑に駆られます。しかし、そうではありません。
イベントソース化されたイベントは、内部状態を再構築するために使用される永続化レコードです。OrderPlacedのような実際のビジネス上の事実を表現するかもしれませんが、その名前、スキーマ、および進化は、依然としてプロデューサーのモデルと永続化のニーズに役立ちます。
変更データキャプチャストリームは、より低レベルです。これは、データベースの変更の行レベルのフィードであり、通常はビジネス用語で変更の理由を表現せずに、保存されたデータがどのように変更されたかを記述します。
どちらのストリームも内部契約であり、公開インテグレーション契約ではありません。
OrderRow.status changed from 2 to 5 のようなCDCレコードは、コンシューマーにステータス5が「履行済み」を意味することを知り、データベースの変更からビジネス上の事実を推測することを強制します。
イベントソース化されたストリームを公開することは、より高い意味論的レベルで同様の結合を作成します。コンシューマーは、プロデューサーがモデルを進化させるにつれて分割、マージ、または再形成する必要がある可能性のあるレコードに依存するようになります。
どちらのストリームも、インテグレーションイベントが派生する内部ソースとして機能できます。
コンシューマーが追加のデータを必要とする場合
コンシューマーは、事実に対応するために、プロデューサーコンテキストが所有するデータを必要とすることがよくあります。一般的なアプローチは3つあり、それぞれ自律性と結合度の間で異なるトレードオフがあります。
イベント伝達状態転送(Event-carried state transfer):各イベントには、事実に対応するためにプロデューサーが所有するデータが含まれます。コンシューマーは、ペイロードのサイズが大きくなり、データが重複するコストをかけて、プロデューサーに呼び出しを行うことなく対応できます。上記のorder-ready-for-fulfillmentイベントはこのアプローチを採用しており、アイテムを含んでいるため、履行側は決して