アーキテクチャ●●●●○

イベントソーシング

現在の状態ではなく、状態変化を起こしたイベントの列を真実として保存する設計。

概要

イベントソーシングは、データの「現在の状態」ではなく、状態を変化させてきた「イベントの列」を真実(source of truth)として保存する設計です。銀行口座を例にすると、ふつうは「残高 3,000円」という現在値を保存しますが、イベントソーシングでは「1,000円入金」「5,000円入金」「3,000円出金」という出来事を発生順にすべて記録し、残高はそれらを足し合わせて導く、という考え方をします。

つまり「今どうなっているか」は保存せず、「何が起きたか」だけを追記していきます。現在の状態が欲しければ、先頭からイベントを順に再生(リプレイ)して積み上げれば求まります。会計の元帳(一度書いたら消さず、修正も新しい記帳で表す)とよく似た、追記専用(append-only)の記録モデルです。監査ログが最初から組み込まれているような設計、と言い換えることもできます。

なぜ生まれたか

一般的なデータベースは、行を上書き(UPDATE)して現在の値を持ちます。この方式は「今の値」を読むには最速ですが、上書きの瞬間に過去が消えます。残高が 3,000円だとして、それがどんな入出金の積み重ねでそうなったのか、いつ・なぜその値になったのかは、履歴を別途ログに残していなければ復元できません。

そのため多くのシステムが、業務テーブルとは別に監査ログや履歴テーブルを後付けし、二重に書き込んで整合を保つ、という手間をかけてきました。それでも「なぜこの状態になったのか」を完全に追うのは難しく、金融や在庫のように過去の全経緯が問われる領域では大きな弱点でした。加えて、マイクロサービスで状態変化を他サービスに伝えたいとき、「変わった結果」だけでなく「何が起きたか」をメッセージキューに流したい需要も高まりました。

イベントソーシングは、この「状態を上書きして過去を捨てる」前提をひっくり返します。起きた出来事そのものを一次データとして残せば、監査ログは仕組みとして最初から存在し、現在の状態はいつでも再計算でき、状態変化はそのまま他システムへ配信できるイベントになる — こうして履歴の喪失という積年の課題を根本から解いたのが、この設計です。

詳細

イベントを積み上げて状態を導く

イベントソーシングの中核は、「状態はイベントの畳み込み(左からの積み上げ)である」という等式です。空の初期状態から始めて、記録されたイベントを1つずつ適用していくと、任意の時点の状態が再現できます。次の図は、保存されたイベント列をイベントストアから読み出し、順に再生して現在の残高を組み立てる流れです。

イベントストアアプリイベントストアアプリ0 から 1000 から 6000 から 3000 円この口座のイベントを全部ください1000円入金5000円入金3000円出金順に適用して残高を積み上げる

図のように、現在の残高 3,000円はどこにも保存されておらず、3つのイベントを再生した結果として毎回導かれます。この「保存されているのはイベントだけ、状態は計算で出す」という性質が、イベントソーシングのすべての利点と難しさの源になっています。

イベントストアという追記専用の器

イベントを保存する場所をイベントストアと呼びます。最大の特徴は追記専用(append-only)で、一度書いたイベントは書き換えも削除もしないことです。「間違えた」ときも過去のイベントを消すのではなく、「取り消し」という新しいイベントを追記して表現します。これにより、どの時点でも「その瞬間までのイベント列」を再生すれば当時の状態を再現でき、いわばタイムマシンのように過去にさかのぼれます。

追記しかしないので書き込みは競合が少なく高速ですが、毎回すべてのイベントを再生していては、履歴が数万件に育つと現在状態を求めるだけで重くなります。そこで実務では、ある時点までの積み上げ結果を「スナップショット」として定期的に保存し、以降はスナップショット+差分のイベントだけを再生することで再生コストを抑えます。スナップショットはあくまで再計算のキャッシュであり、真実はあくまでイベント列のほうです。

CQRS との組み合わせと落とし穴

イベントソーシングは、書き込みモデルと読み取りモデルを分離するCQRSとしばしばセットで語られます。イベント列は「何が起きたか」を厳密に記録するのには最適ですが、「現在の一覧を条件で検索する」といった読み取りには向きません。そこで、イベントが追記されるたびにそれを購読して、検索や表示に最適化した別の読み取り用データ(リードモデル、投影=プロジェクションとも呼ぶ)を組み立てておきます。書き込みはイベントストアへ、読み取りは投影から、という役割分担です。

強力な設計ですが落とし穴もあります。まず、一度書いたイベントの形式(スキーマ)を後から変えにくいことです。過去のイベントは未来永劫再生され続けるため、フィールドの意味を変えると古いイベントの再生が壊れます。バージョンを付けて古い形式も読めるようにする「イベントの進化」への配慮が要ります。また、イベントから投影を作るのは非同期になりがちで、書いた直後に読むとまだ反映されていない、という結果整合性の性質が表に出ます。設計としての難度が高いぶん、履歴が本質的に重要な領域(金融、在庫、監査が厳しい業務)に絞って使うのが現実的で、あらゆる CRUD をこれで置き換えようとするのは過剰になりがちです。