UPDATE文を実行するたびに、何かが消える。古い値 — さっきまでそこにあったもの — が消える。ほとんどのデータベースは忘れるように設計されている。すべてのUPDATEが以前のものを上書きし、すべてのDELETEが完全に削除し、アプリケーションには現在のスナップショットだけが残る。
これは自然だから受け入れている。でもシステムが別の質問に答える必要があったら?「現在の状態は何か?」ではなく「ここにどうやってたどり着いた?」
それがイベントソーシングが答える質問だ。
銀行口座を考えてみよう。CRUDモデルでは:
残高が$1,100だとわかる。それが入金だったのか、返金だったのか、修正だったのか、バグだったのかはわからない。履歴は消去された。
不正調査員が「この口座の過去30日のすべての取引を見せてください」と求めたとしよう。CRUDでは、別の監査ログが必要だ。そしてその監査ログは常に後付け — 取ってつけたもの、一貫性がなく、決して完全ではない。
現在の状態を保存する代わりに、それを生成したイベントのシーケンスを保存する:
現在の状態は導出されるもので、保存されるものではない。イベントをリプレイすればいつでも取得できる。しかしCRUDでは決して答えられなかった質問にも答えられる:
イベントログが真実の源泉だ。現在の状態は単なるキャッシュされた計算結果に過ぎない。
金融システム。 銀行、決済処理業者、トレーディングプラットフォーム。規制要件で完全な監査証跡が義務付けられることが多い。イベントソーシングならこれが無料で手に入る — 後付けではなく、アーキテクチャそのものだ。
共同編集。 Google Docsはドキュメントを保存していない — すべてのキーストローク、すべての削除、すべてのカーソル移動を保存している。ドキュメントはすべての操作をリプレイした結果だ。
ECの注文管理。 注文は単に「出荷済み」ではない。注文され、確認され、支払い承認され、ピッキングされ、梱包され、出荷され、そして部分的に返品されたかもしれない。各ステップがカスタマーサポート、分析、紛争解決に重要だ。
本番問題のデバッグ。 何かがうまくいかないとき、障害の時点までイベントをリプレイできる。システム状態のDVRのようなものだ。
イベントは追記のみ。永遠に蓄積する。高トラフィックのシステムは1日に数百万のイベントを生成する。戦略が必要だ:
イベントストアが真実の源泉なら、読み取りモデルはイベントから構築される必要がある。イベントの書き込みと読み取りモデルの更新の間に遅延がある。UIがミリ秒から数秒間古いデータを表示する可能性がある。
書き込み:イベント追記 → イベントストア
読み取り:イベントストア → プロジェクション → 読み取りデータベース → UI
プロジェクションステップが遅延を生む。
イベントは不変だ。遡ってイベントv1をイベントv2に変更できない。バージョニングとアップキャスト戦略が必要だ:
「先月$1,000以上を入金したすべてのユーザー」を問い合わせたい?CRUDならSQLクエリだ。イベントソーシングではプロジェクション — そのクエリパターン専用に構築された読み取りモデル — が必要だ。異なるユースケースのために複数のプロジェクションを維持することになるだろう。
イベントソーシングはほぼ必ずCQRS(コマンドクエリ責務分離) と組み合わせる:
コマンド → バリデーション → イベント追記 → イベントストア
↓
プロジェクション
↓
読み取りモデル(SQL、Elasticなど)
↓
クエリ
書き込みモデルと読み取りモデルは異なるデータ構造で、それぞれの目的に最適化されている。イベントストアがそれらをつなぐ。
イベントソーシングを使うとき:
イベントソーシングを使わないとき:
イベントソーシングは強力だが要求も高い。CRUDが文字通り解決できない問題 — 完全な監査証跡、時間的クエリ、リプレイベースのデバッグ — を解決する。しかしほとんどのアプリケーションが必要としない複雑さを追加する。
銀行システムを構築していてイベントソーシングを使っていないなら、おそらく監査ログを取ってつけて一貫性があることを祈っているだろう。ブログを構築していてイベントソーシングを使っているなら、自転車にジェットエンジンを搭載しているようなものだ。
ドメインを知れ。それに応じて選べ。
目的地だけでなく旅路を保存しろ — ただし旅路が重要な場合だけ。
— blanho
冗長パイプライン、インテリジェントなセグメント選択、カスタムストレージレイヤー — Netflix Live Originアーキテクチャの内側。
ロギング、認証、リトライ、レート制限 — 最初に設計しないが、後で全員が苦しむもの。
どのアーキテクチャも1つの問題を解決し、3つの新しい問題を生む。コミットする前に誰も教えてくれないことがここにある。
-- ユーザーが$100を入金
UPDATE accounts SET balance = 1100 WHERE id = 42;
-- 以前の残高は?いつ変更された?なぜ?
-- すべて消えた。// UPDATE balance = 1100の代わりに
const events = [
{ type: "AccountOpened", amount: 0, timestamp: "2026-01-01" },
{ type: "MoneyDeposited", amount: 500, timestamp: "2026-01-15" },
{ type: "MoneyDeposited", amount: 300, timestamp: "2026-02-01" },
{ type: "MoneyWithdrawn", amount: 200, timestamp: "2026-02-10" },
{ type: "MoneyDeposited", amount: 500, timestamp: "2026-03-01" },
];
// 現在の残高 = すべてのイベントをリプレイ
// 0 + 500 + 300 - 200 + 500 = 1100// イベント v1(2024年)
{ type: "UserRegistered", name: "Alice" }
// イベント v2(2025年)- emailを追加
{ type: "UserRegistered", name: "Alice", email: "alice@example.com" }
// アップキャスター:v1イベントを読むとき、v2に変換
function upcast(event) {
if (event.version === 1) {
return { ...event, email: null, version: 2 };
}
return event;
}