🎫

比較的シンプルなドメインイベントの利用方法

2022/02/19に公開

ドメインイベントとは

ドメインイベントは DDD に登場するドメインオブジェクトのひとつで,あるドメインで発生する出来事を表現したものです.ドメインの中では,「〜が行われた時」や「もし〜になったら」といったような特定の出来事の発生を契機に別の何かを行うことがあります.この出来事の発生をドメインイベントとして表現し,ドメインイベントが生成されたらそれをパブリッシャが出版 (publish) し,サブスクライバがドメインイベントを購読 (subscribe) して何らかの処理を行います.

ドメインイベントの使い道として典型的なのは,結果整合性を用いてある集約が変更されたときに別の集約を非同期で変更するといったものです.たとえば,メッセージが投稿された時に,そのメッセージの通知を非同期で作成するといったものが考えられます.同一トランザクションで複数の集約を同期的に変更することは非推奨とされている[1]ため,ある集約の変更に伴って別の集約を変更する場合はドメインイベントを用いることになります.

ドメインイベントは非常に強力なツールでとても便利ではあるのですが,世のドメインイベントの利用方法を見ると中々複雑で少しハードルの高い代物だと感じます.そこで,今回は比較的シンプルなドメインイベントの利用方法を紹介したいと思います.

ドメインイベントの利用

IDDD本では,ドメインイベントの発行方法として軽量なオブザーバ (パブリッシャとサブスクライバ) を作る方法が紹介されています.この方法では,ドメインオブジェクトから発行されたドメインイベントをパブリッシャが出版し,パブリッシャに登録されているサブスクライバが購読して,ドメインイベントをデータストアに格納したり,直接他のサービスに転送したりします.基本的には,格納されたり転送されたイベントをまた別のサブスクライバ (リモートのサブスクライバ) が購読し,他の集約を変更したりします.

しかし,今回は次のような方針を採用してみようと思います.

  • ドメインイベントを集約の一部として扱う
  • 集約を保存するデータストアと同じデータストアにドメインイベントを保存する
  • リモートのサブスクライバが API 経由でドメインイベントを取得する


↑ それっぽい処理流れの図

この方針について詳しく説明します.

ドメインイベントを集約の一部として扱う

IDDD本で紹介されている軽量なパブリッシャを利用する手法では,ドメインイベントは集約の中で生成され,パブリッシャによって出版されます.つまり,集約の中でパブリッシャを利用する形になっています.しかし,集約の中でパブリッシャを利用するのはテストのしやすさの観点からも避けたいですし,パブリッシャが集約に必要な属性なのかと言われると微妙なところでもあります.

ローカルにパブリッシャとサブスクライバを用意することで,様々なサブスクライバを用意して,ドメインイベントごとに異なる処理を行えます.たとえば,同期的にメールを送信したり,データストアにドメインイベントを格納したり,他のサービスにドメインイベントを転送したりといった処理が挙げられます.しかし,結果整合性が許容できるのであれば,単純にドメインイベントをデータストアに格納し,リモートのサブスクライバに処理を任せるというやり方でも十分なケースは多いように思います.

そこで,今回はそもそもローカルにパブリッシャを用意せず,ドメインイベントを集約の一部として保持し,集約と一緒に保存するというアプローチを選択してみます.

上記のメッセージ集約 (Message Aggregate) は,MessageId や MessageText といった値オブジェクト (Value Object) と同様に MessageEvent というドメインイベント (Domain Event) を保持しています.ドメインイベントは MessagePosted と MessageEdited の2種類があり,メッセージが新規作成されたときに前者が生成され,メッセージが変更された時に後者が生成されます.そして,この集約が永続化される時に生成されたドメインイベントも永続化されます.

この手法の懸念点は,集約が一時的なドメインイベントを保持してしまうことです.集約が今まで起こった全てのドメインイベントを保持するのは現実的ではないので,あるトランザクションで発生したドメインイベントのみを保持するという方針になります.集約を介して永続化されるのに,集約を再構築した際は失われる属性というのは,他の属性と大きく性質が異なっています.しかし,実害はないですし,方針として明確になっていれば大きな問題にはならないと思います.

また,この手法ではローカルのパブリッシャとサブスクライバを用意しないので,処理の自由度は下がります.たとえば,あるドメインイベントに対しては同期的に何か別の処理をしたい場合には向いていません.

集約を保存するデータストアと同じデータストアにドメインイベントを保存する

ドメインイベントを集約の一部として扱う場合,自然とドメインイベントを保存する先も集約を保存するのと同じデータストアになります.これによって,集約の保存とドメインイベントの発行の整合性を気にする必要がなくなります.集約の保存先とドメインイベントの格納先 (または転送先) が異なる場合,二つの処理が両方成功するか両方失敗するかのどちらかになるように整合性を保つ必要があります.

また,同じデータストアを用いることでサービスの依存先が減り,自立型のサービスになることもメリットのひとつです.サービスの依存先が減れば減るほど,サービスの可用性も高まりますし,処理も単純になります.

この手法はIDDD本の8.5章で紹介されているイベントストアの一種になると思います.紹介されているイベントストアと違うのはドメインイベントを集約の一部として扱っている点で,概念的には同じものです.

リモートのサブスクライバが API 経由でドメインイベントを取得する

データストアに格納したドメインイベントをリモートのサブスクライバが受け取って処理できるように,ドメインイベントの発行元にドメインイベントの取得用の API を提供させます.ここでの"リモートのサブスクライバ"とは,ドメインベントを元に非同期で何らかの処理を行う主体のことです.このように,ドメインイベントをプッシュ型ではなくプル型で渡すことでサービスの依存先が減り,自立型なサービスになります.

この手法もIDDD本の8.6章の「RESTfulなリソースによる、通知の発行」で紹介されているものと同じです.必ずしも REST API である必要はなく,gRPCなどでも良いでしょう.重要なのはプル型であることです.ただし,異なる境界づけられたコンテキストから直接データストアにアクセスしてドメインイベントを取得するのは避けましょう.これは「共有データベース」というアンチパターンです.

リモートのサブスクライバがドメインイベントを取得する方法には,次のデメリットも存在します.

リモートのサブスクライバがポーリングでドメインイベントを取得する必要がある

ドメインイベントの発行元はデータストアへの保存しかしないので,リモートのサブスクライバがドメインイベントを定期的に取得しなければなりません.ポーリングの間隔が短すぎると負荷が高くなる可能性がありますし,間隔を長くするとユーザ体験が低下する恐れがあります.

しかし,ドメインイベントは不変なのでキャッシュとの相性が良いですし,相当な頻度でなければ負荷が問題になることは少なそうです. gRPC の Server Stream RPC とか使うと相性良いような気がします.むしろ,必要以上にリモートのサブスクライバを起動させ続けないといけないコストの方が懸念視されるかもしれません.そこを気にするのであれば,この手法は選択しない方が良さそうです.

ドメインイベントの多重処理を防ぐために,リモートのサブスクライバが重複チェックを行う必要がある

ドメインイベントの発行元は,そのドメインイベントがどのように使われたかを気にしないため,リモートのサブスクライバ側がどのドメインイベントを既に取得したか管理する必要があります.

一番簡単なのは,ドメインイベントに発生時刻を持たせて,どの時刻までのドメインイベントを処理したかをリモートのサブスクライバ側が保持しておくというやり方です.ドメインイベントに連番のIDを持たせて,どのIDまで取得したかを保持しておくのでも構いません.

ある処理のために異なるサービスの異なるドメインイベントが必要になってくると辛い

サービスが複雑になり,ある非同期処理のために複数のサービスからドメインイベントを取得しなければならず,さらにドメインイベント間の順序関係も考慮する必要が出てくると,この手法は辛くなります.このような場合は統合されたキューを使う方が便利です.

まとめ

この記事ではローカルのパブリッシャ,サブスクライバを用いない,比較的シンプルなドメインイベントの利用方法について紹介しました[2]

この手法は,仕組みがシンプルになり,サービスの依存先も減るといった利点がありますが,反面ドメインイベント発行時の自由度は下がってしまうなどの欠点もあります.要件に合わせて選択できると良いかなと思います.

サンプルコードも書こうかと思いましたが,文章だけでお腹いっぱいになったので,需要ありそうであれば (かつ元気が出れば) 実装編にもチャレンジしたいと思います.

脚注
  1. DDDを提唱した Eric Evans さんやIDDD本の作者の Vaughn Vernon さんも異なる集約を同一トランザクションで変更することを非推奨としています.一方で,「ドメイン駆動設計 サンプルコード&FAQ」を執筆された松岡さんは,異なる集約を同期的に変更することは,メリットデメリットを考慮した上で判断できていれば問題なく,十分実践的であると述べられています. ↩︎

  2. 「十分複雑だろ!」というツッコミはあるかと思いますが...... ↩︎

Discussion