メインコンテンツまでスキップ

イベントソーシング

イベントの履歴がデータの本体

通常のアプリケーションは「現在の状態」を保存します。イベントソーシングは「起きたことの履歴」を保存し、現在の状態はその履歴から導出します。


従来のアプローチとの違い

従来: 状態を直接保存(State Sourcing)

User テーブル:
┌──────┬──────────┬─────────────┬──────────────┐
│ id │ name │ login_count │ last_login │
├──────┼──────────┼─────────────┼──────────────┤
│ 1 │ Alice │ 42 │ 2026-03-25 │
└──────┴──────────┴─────────────┴──────────────┘

→ 現在の状態だけがわかる
→ なぜ42回なのか、いつ増えたのかはわからない
→ UPDATE のたびに過去が消える

イベントソーシング: 履歴を保存

Events テーブル:
┌────┬────────────────────┬─────────┬──────────────────────┐
│ # │ type │ user_id │ data │
├────┼────────────────────┼─────────┼──────────────────────┤
│ 1 │ UserRegistered │ 1 │ {name: "Alice"} │
│ 2 │ LoginSucceeded │ 1 │ {ip: "1.2.3.4"} │
│ 3 │ LoginSucceeded │ 1 │ {ip: "5.6.7.8"} │
│ 4 │ NameChanged │ 1 │ {name: "Alice S."} │
│ 5 │ LoginFailed │ 1 │ {reason: "bad pass"} │
│ 6 │ LoginSucceeded │ 1 │ {ip: "1.2.3.4"} │
│... │ ... │ ... │ ... │
│ 44 │ LoginSucceeded │ 1 │ {ip: "9.0.1.2"} │
└────┴────────────────────┴─────────┴──────────────────────┘

→ 現在の状態: イベントを先頭から再生して導出
name = "Alice S."(#4 で変更)
login_count = 42(LoginSucceeded を数える)
last_login = 2026-03-25(#44 のタイムスタンプ)

→ 過去の任意の時点の状態も復元できる
「3月1日時点のログイン回数は?」→ #1〜#N のうち3/1以前を再生

イベントソーシングのメリット

1. 完全な監査証跡

従来:
Q: 「なぜこのユーザーのステータスが locked なのか?」
A: 「さあ...UPDATE されたことしかわからない」

イベントソーシング:
Q: 「なぜこのユーザーのステータスが locked なのか?」
A: イベント履歴を見ればわかる:
#30 LoginFailed(reason: "bad password")
#31 LoginFailed(reason: "bad password")
#32 LoginFailed(reason: "bad password")
#33 AccountLocked(reason: "3 consecutive failures")

2. 時間旅行(Temporal Query)

「このユーザーの3月1日時点の状態は?」
→ 3月1日以前のイベントだけを再生すれば復元できる

「先月のアクティブユーザー数は?」
→ 先月の LoginSucceeded イベントの distinct user_id を数えるだけ

3. バグの再現とデバッグ

「なぜこの状態になったのか?」
→ イベント履歴を見ればわかる
→ 同じイベント列を別環境で再生すれば完全に再現できる

4. 後から新しい読み取りモデルを追加できる

半年前: 「ログイン回数だけ集計すればいい」
今: 「テナント別の時間帯別ログイン分布も欲しい」

従来: 過去のデータがないから対応不可
イベントソーシング: 過去のイベントを新しい視点で再集計するだけ

イベントソーシングのデメリット

万能ではありません。トレードオフを理解して採用を判断します。

1. 現在の状態の取得が遅い

「ユーザー #1 の現在の名前は?」

従来: SELECT name FROM users WHERE id = 1 → 即座

イベントソーシング: 全イベントを再生して名前を導出
→ イベントが1万件あると遅い
→ 対策: スナップショットを定期的に作る

2. イベントスキーマの進化が難しい

v1: LoginSucceeded(userId, timestamp)
v2: LoginSucceeded(userId, timestamp, ipAddress) ← フィールド追加

過去のv1イベントにはipAddressがない
→ スキーマ進化の戦略(アップキャスト)が必要

3. 学習コストと実装複雑性

従来: INSERT / UPDATE / SELECT で完結
イベントソーシング: イベントストア、プロジェクション、
スナップショット、スキーマ進化...
→ チーム全員が理解する必要がある

「ほぼイベントソーシング」のパターン

完全なイベントソーシングを採用しなくても、イベントを追記で保存するだけで多くの恩恵を得られます。

┌─────────────────────────────────────────────────────────────┐
│ 採用レベルの段階 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Level 0: 状態のみ保存(従来) │
│ → UPDATE users SET login_count = login_count + 1 │
│ │
│ Level 1: イベントログを追記 ← 多くのシステムはここで十分 │
│ → INSERT INTO events (type, data, timestamp) │
│ → 状態は従来通り UPDATE で管理 │
│ → イベントは監査・分析用 │
│ │
│ Level 2: イベント + CQRS │
│ → イベントを書き込み、読み取りモデルは別DBで構築 │
│ → PostgreSQL + ClickHouse のデュアル構成 │
│ │
│ Level 3: 完全なイベントソーシング │
│ → イベントがデータの本体 │
│ → 現在の状態はイベントの再生で導出 │
│ → スナップショット、スキーマ進化の管理が必要 │
│ │
└─────────────────────────────────────────────────────────────┘

Level 1(イベントログ追記)は既に多くのシステムが実践しています。 認証ログ、アクセスログ、操作履歴など、「起きたことをINSERTで記録する」のはイベントソーシングの最も基本的な形です。


まとめ

┌────────────────────────────────────────────────────────────┐
│ イベントソーシングの本質 │
├────────────────────────────────────────────────────────────┤
│ │
│ 「何が起きたか」を消さずに全部残す │
│ │
│ これだけで: │
│ ・完全な監査証跡が得られる │
│ ・過去の任意の時点を復元できる │
│ ・後から新しい集計・分析ができる │
│ ・バグの原因を追跡できる │
│ │
│ 全部採用する必要はない: │
│ ・Level 1(イベントログ追記)だけでも大きな価値 │
│ ・必要に応じて Level 2(CQRS)、Level 3 に進む │
│ │
└────────────────────────────────────────────────────────────┘

次のステップ

  • CQRS: 書き込みと読み取りを分離するアーキテクチャ