TL;DR

内容

境界をまたぐ更新の難しさ

「注文が確定すると、それに対応する請求書が必ず作成される」という要件を考える。請求書の発行は「受注」ドメインではなく「請求」ドメインの仕事である。2 つは別々の境界づけられたコンテキストに分かれているので、境界を越えて相手のオブジェクトを直接操作できない。

素朴に考えると、請求コンテキストに公開 API を用意して、次のように処理することになる。

Ask billing context to create invoice
if successfully created:
  create order is order-taking context

しかし、どちらかの更新が失敗した場合を考慮しなければならず、複雑になる。別々のシステム間で更新を同期させる方法として2 フェーズコミットがあるが、本書はそれが必要になることはないとする。現実の世界でも、サブシステムが 1 つの段階を終えるのを待ってから次の段階に移ることはない(スターバックスは2フェーズコミットを使わない)。

メッセージによる非同期の調整

代わりに、メッセージ(イベント)を使って非同期に調整する。まれにうまくいかないこともあるが、まれなエラーに対処するコストは、すべてを同期させるコストよりはるかに小さいことが多い。たとえば、請求書の作成をその場で要求せず、請求ドメインにメッセージを送って、残りの注文処理を続ける。

メッセージが失われた場合の対応には、次の 3 つがある。

  1. 何もしない。顧客は無料で商品を手に入れ、企業はそのコストを負担する。コストが小さいなら、これが適切な解決策になる。
  2. メッセージが失われたことを検知して再送する。受信側と送信側のデータを比較し、一致しなければエラーとする。
  3. 前のアクションを取り消す、またはエラーを修正する補償アクションを作る。注文をキャンセルして商品の返送を求めたり、返金したりする。

整合性が求められる場合は、2 か 3 を選ぶ必要がある。このとき整合性が取れるのは、ある程度時間が経ってからになる。これを結果整合性と呼ぶ。

結果整合性の例

商品の価格が変わったときに、まだ発送されていない注文をすべて更新するケースを考える。すぐに整合性を取る必要があるなら、商品レコードを変更するたびに、影響を受けるすべての注文も更新しなければならず、時間がかかる。

すぐに整合性を取る必要がなければ、「価格が変更された」(PriceChanged)ドメインイベントを作る。このイベントが「変更済みの価格で注文を更新する」(UpdateOrderWithChangePrice)コマンドのきっかけになる。このコマンドは、商品の価格が変わってからしばらくして処理される。

キーワード

自分の理解

補償アクションはイベントソーシングと関係があるのではないかと思っているが、まだ確かめていない。