Event Sourcing が応える課題 ── CRUD で失われていたもの
前章で見たように、Event Sourcing は「現在の状態を保存する」のではなく「起きた事実を記録する」設計だ。だが、それが何の役に立つのか?
この問いに「履歴を取れる」とだけ答えるのは浅い。実際には Event Sourcing は 6つの構造的な問題に同時に応える 設計であり、その全体像を知ることが採用判断の精度を上げる。
この章では、CRUD ベースの設計で起きがちな具体的な「困りごと」を採用管理システムを題材に再現し、Event Sourcing がどう応えるかを対比する。最後に、Event Sourcing が向かない領域 も明確にする。
CRUD ベースの設計、その典型
まず、Event Sourcing を使わない場合の標準的な設計を確認する。採用管理システムの選考プロセスを、CRUD で素直に作るとこうなる。
-- 選考プロセスのテーブル
CREATE TABLE screening_processes (
id UUID PRIMARY KEY,
candidate_id UUID NOT NULL,
job_id UUID NOT NULL,
current_stage TEXT NOT NULL, -- 'document' | 'first_interview' | ...
updated_at TIMESTAMP NOT NULL,
updated_by UUID NOT NULL
);
選考が次のステージへ進めば、current_stage を UPDATE する。シンプルで素直だ。多くのシステムはこれで十分動く。
しかし、ある日こんな問い合わせが来る。
法務部より:
「この候補者から個人情報開示請求があった。
2025年12月時点での選考状態と、それ以降の状態変更を全て出してほしい」
ここでようやく、CRUD 設計が抱えていた問題が水面下から浮上する。
問題1:時間が失われる ── 過去の状態を再現できない
CRUD で UPDATE すると、前の値は消える。current_stage を first_interview から second_interview に書き換えた瞬間、「以前は first_interview だった」という事実は記録上どこにも残らない。
2025-12-01: current_stage = 'document'
2025-12-15: current_stage = 'first_interview' ← 上書き
2026-01-10: current_stage = 'second_interview' ← 上書き
2026-02-01: current_stage = 'final_interview' ← 上書き
↓ 現在テーブルに残っているのは:
current_stage = 'final_interview'
updated_at = '2026-02-01'
→ 「2025-12時点で document だった」という情報は失われている
これに対する典型的な対処は 履歴テーブル を別に作ることだ。
CREATE TABLE screening_process_history (
id UUID PRIMARY KEY,
process_id UUID NOT NULL,
current_stage TEXT NOT NULL,
changed_at TIMESTAMP NOT NULL,
changed_by UUID NOT NULL
);
UPDATE 時にトリガーで履歴テーブルに INSERT する。よくあるパターンだ。だが、これには副作用がある。
履歴テーブル方式の問題:
1. メインテーブルと履歴テーブルがズレるリスク
(トリガー漏れ・バルク UPDATE での記録漏れ)
2. 履歴の「真正性」が後から保証しづらい
(メインテーブルを正と見なすと、履歴はあくまで派生物)
3. 「ステージが変わった」しか記録されず、「なぜ変わったか」が残らない
4. 関連集約(候補者・面接記録など)も同様の履歴設計が必要になり、
アドホックに肥大化する
Event Sourcing では、そもそも UPDATE が存在しない。状態変更は FirstInterviewCompleted のようなイベントとして追記され、current_stage は派生物だ。「履歴と現在状態がズレる」問題は構造的に発生し得ない。
問題2:意図が失われる ── 「なぜ」が分からない
CRUD で記録できるのは「何が、いつ、誰によって」までだ。「なぜ」は記録上に残らない。
履歴テーブルの行:
process_id = 'sp-001'
stage = 'final_interview'
changed_at = '2026-01-15 14:23:00'
changed_by = 'user-recruiter-7'
→ 二次面接をスキップしたのか?
それとも何らかの理由で取り消されて再設定されたのか?
候補者からの辞退申し出があって戻したのか?
── どれも分からない
業務上、状態変更には常に理由がある。Event Sourcing はその理由をイベントの 型 で表す。
// CRUDなら同じ「current_stage を変える UPDATE」になるが、ESでは別の型になる
class CandidateWithdrew // 候補者が辞退(候補者起点・選考終了)
class ScreeningCancelledByCompany // 会社都合の取り消し(会社起点・選考終了)
class FirstInterviewRescheduled // 一次面接の再設定(双方都合・選考継続)
イベントの型が業務上の意図を表す。この情報は CRUD 上では「changed_by と changed_at だけでは復元できない」ものだ。
採用管理のように、業務判断の理由が後から問われるドメイン では、この差は決定的に効いてくる。
問題3:「分析」と「業務」のデータがズレる
CRUD 設計では、業務処理用のデータベースと、分析・レポート用のデータベースが別物になることが多い。「データウェアハウスに ETL する」「BIツール用のリードレプリカを作る」── どこかで非正規化されたコピーを作る。
通常の構造:
業務DB(current_stage が常に最新)
│
↓ ETL(深夜バッチ・CDCなど)
分析DB(履歴テーブル+スナップショット)
│
↓
BIツール/ダッシュボード
ここで何が問題か? 業務 DB に保存されている情報以上のことは、分析 DB に流せない という制約だ。「なぜステージが変わったか」が業務 DB に保存されていないなら、分析 DB にも入らない。
Event Sourcing の場合、イベントストリームそのものが業務処理であり、分析の入力でもある。
graph LR
subgraph crud_struct["CRUD + ETL(従来)"]
BD[("業務DB<br/>current_stage<br/>のみ最新")]
ETL["ETL<br/>(深夜バッチ・CDC)"]
AD[("分析DB<br/>履歴+スナップショット")]
BI["BIツール<br/>ダッシュボード"]
BD --> ETL --> AD --> BI
BD -.失われた情報は.-> X["❌ 流せない"]
end
subgraph es_struct["Event Sourcing"]
IS[("イベントストリーム<br/>業務処理=記録")]
IS --> P1["業務用<br/>Projection"]
IS --> P2["分析用<br/>Projection"]
IS --> P3["監査用<br/>Projection"]
IS --> P4["ML用<br/>ストリーム"]
end
style crud_struct fill:#ffe8e8,stroke:#cc6666
style es_struct fill:#e8f5e9,stroke:#43a047
style X fill:#ffcdd2
業務処理の副産物として「分析・監査・ML への入力」が自然に得られる。これが近年 Event Sourcing が AI / 分析パイプライン領域で再評価される理由だ。
問題4:監査要件への後付け対応が困難
GDPR・SOX・金融規制・医療法 ── 業界・地域によって異なるが、共通しているのは「変更履歴の完全性」の要求だ。「いつ・誰が・何を・なぜ変えたか」を後から完全に検証できる必要がある。
CRUD 設計のシステムに、これを後付けで導入するのは難しい。
後付けの典型的な失敗パターン:
1. 履歴テーブルを追加した
→ 既存コードの UPDATE 全箇所にトリガー or アプリ側追記が必要
→ 漏れが発生し、監査証跡に「穴」ができる
2. 監査ログテーブルを追加した
→ 業務ロジックと監査ログ書き込みが二重化
→ トランザクション境界が曖昧になり、ログ書き込みだけ漏れることがある
3. CDC(Change Data Capture)を導入した
→ 「データが変わった」は取れるが「なぜ変わったか」は取れない
→ 結局、監査証跡として不十分
Event Sourcing では、業務処理 = イベント追記 なので、監査証跡が業務処理と物理的に同一だ。「業務は動いたが監査ログが落ちた」という状態は構造的に起きない。
採用管理のように 個人情報を扱い、開示請求対応が必要なドメイン では、この特性は単なる便利機能ではなく 法的要件への自然な対応 になる。
問題5:時間軸を持つビジネスロジックが書きづらい
「過去30日間に書類選考を通過した候補者の数」「前四半期と今四半期での通過率の比較」── 業務ロジックの中には、本質的に時間軸を伴うものがある。
CRUD 設計でこれを実現するには、選択肢は限られる。
選択肢A:履歴テーブルから集計する
→ SQL が複雑になる(過去X日時点の状態を再構築する)
→ 大量データだと遅い
選択肢B:定期スナップショットを取る
→ ストレージとバッチ運用のオーバーヘッド
→ スナップショット間隔より細かい時間粒度は取れない
選択肢C:イベントログを別途持つ
→ 結局 Event Sourcing の劣化版になる
Event Sourcing なら、「ある時点までイベントを再生して当時の状態を再構築する」が標準操作 だ。
// 任意時点での状態を再構築
const events = eventStore.readStream(
'screening-process-sp-001',
{ until: new Date('2026-01-01') }
)
const stateAtJan1 = ScreeningProcess.reconstitute(events)
時間軸を業務ロジックに組み込みやすい。Time-travel debug もそのまま実現できる。
問題6:集約境界を「後から動かしたい」要求への耐性
DDD の集約は、「常に整合性が保たれるべき範囲の境界」 だ。だが現実のシステムでは、業務ルールが変わって境界を見直したくなることがある。
よくあるシナリオ:
当初設計:「選考プロセス」と「面接記録」は別集約
数ヶ月後:「最終面接の結果が出る前に内定通知を出してはいけない」
というルールが追加された
→ 「面接記録」と「内定通知」をまたいだ整合性が必要になる
CRUD ベース+通常の集約設計では、この変化に対応するには集約をリファクタリングするしかない。リポジトリの実装、トランザクション境界、API 契約 ── 多くが連動して変わる。
Event Sourcing では、過去の事実が「型を持ったイベント」として残っている。集約を再設計したとき、新しい集約は 同じイベントストリームから再構築 できる。
元の設計:
ScreeningProcess 集約 ← screening-process-* ストリーム
InterviewRecord 集約 ← interview-record-* ストリーム
新しい設計:
ScreeningProcess 集約 ← screening-process-* ストリーム
+ interview-record-* ストリーム を読み込む
→ 過去のイベントはそのまま、集約だけ再構築できる
特に2023年以降、Sara Pellegrini が提唱した DCB(Dynamic Consistency Boundary) は、Event Sourcing を前提として、集約の境界を実行時に動的に決める 設計を可能にする。集約という静的な箱をなくし、必要に応じてイベント群を「タグ」で切り出す。これは Event Sourcing なしには成り立たない設計だ。
つまり Event Sourcing は、今すぐ DCB に踏み切らないとしても、将来の境界再設計の余地を保持する 効果がある。
ここまでの整理:Event Sourcing が応える6つの課題
mindmap
root((Event Sourcingが応える6課題))
過去状態の再現
履歴テーブルの限界
監査要件
意図の保持
業務判断の根拠
ドメイン語彙の保持
業務と分析の統一
ETLからの解放
AI/MLパイプライン
監査要件への対応
GDPR・SOX
開示請求対応
時間軸ロジック
Time-travel debug
時系列分析
境界再設計の余地
集約のリファクタリング耐性
DCBへの素地
これら6つは独立した課題ではなく、「現在の状態しか持たない」設計が共通して抱える根本問題の異なる現れ方 だ。Event Sourcing はその根本問題に対して、「主データを変える」というレベルで対応する設計と言える。
一方で:Event Sourcing が向かない領域
「銀の弾丸はない」── Vernon が前作で繰り返し述べていたことは、本シリーズでも一貫する。Event Sourcing が 構造的に向かない領域 がある。
向かない領域A:シンプルな CRUD で十分なドメイン
例:
- 設定値テーブル(システム設定・フィーチャーフラグ)
- マスタデータ(国コード・通貨コード・固定の業種リスト)
- 単純なルックアップテーブル
これらに Event Sourcing を適用するのは、コストに見合わない。「設定値が変わった理由」を残しても、業務上ほぼ使わない。CRUD で UPDATE するのが正しい。
向かない領域B:イベントの「意味」が薄いドメイン
例:
- 単純なログ収集(アクセスログ・APMメトリクス)
- ストリーミング処理(時系列データの集計)
「ドメインで何が起きたか」という意味を持たないデータには、Event Sourcing の利点がほぼ効かない。これらは イベントストリーミング基盤(Kafka など) のほうが向いている。
混同注意:Event Sourcing と Event Streaming は違う。Event Sourcing は「業務状態をイベント列で表現する」設計、Event Streaming は「イベントを流す」インフラ。詳細は ch05 で扱う。
向かない領域C:強い結合・グローバルなトランザクションが必要なドメイン
例:
- 複数集約をまたいだ厳密な ACID 整合性が業務要件
- レイテンシが極限まで重視される単純更新(在庫の即時減算など)
Event Sourcing は読み取りに Projection を介すため、書き込み直後の即時整合性 を素直に取りにくい(後の章で対処法は議論する)。「書いた瞬間に読めて当然」という前提が強いシステムには摩擦が生じる。
向かない領域D:チームに学習コストを払う余裕がない場合
これは技術というより組織の話だ。Event Sourcing は CRUD と異なるメンタルモデル を要求する。チーム全員が「なぜ UPDATE しないのか」「なぜ Read Model が必要なのか」を腹落ちしていないと、運用が破綻する。
チーム導入の最低ライン:
- リードエンジニアが CQRS と Event Sourcing を完全に理解している
- チームメンバーが「変化を記録する」発想に慣れる時間(3-6ヶ月)が確保できる
- 既存 CRUD システムからの段階移行プランがある
これらが満たせない状況で「先進的だから」という理由で導入すると、確実に裏目に出る。
適用判断の最初の問い
採用判断の入り口で、次の3つの問いに答えてみる。
問い1:このドメインで「過去の状態」「変更理由」が業務上意味を持つか?
Yes → Event Sourcing の検討に値する
No → CRUD で十分
問い2:監査・履歴・時間軸ロジックの要求が、設計の重心を占めるか?
Yes → Event Sourcing が自然な選択
No → CRUD + 履歴テーブルで足りる可能性が高い
問い3:チームが新しいメンタルモデルを習得する余裕があるか?
Yes → 導入可能
No → 部分導入か見送りを検討
3つすべてに Yes なら、Event Sourcing は設計上の最有力候補になる。1つでも No があるなら、慎重に判断すべきだ。
quadrantChart
title 適用判断:履歴重要度 × 学習コスト許容度
x-axis "学習コスト払えない" --> "学習コスト払える"
y-axis "履歴・意図が業務上重要でない" --> "履歴・意図が業務上極めて重要"
quadrant-1 "✅ Event Sourcing 最有力"
quadrant-2 "⚠️ 部分導入検討"
quadrant-3 "❌ CRUDで十分"
quadrant-4 "⚠️ CRUD+履歴テーブル"
"採用管理(選考)": [0.7, 0.85]
"求人マスタ": [0.7, 0.2]
"システム設定": [0.3, 0.1]
"金融取引": [0.8, 0.95]
"ユーザー設定値": [0.5, 0.15]
"監査ログ要件": [0.4, 0.7]
採用管理システムへの適用判断
題材としている採用管理システムでは、3問とも Yes になる。
問い1:過去状態と変更理由が意味を持つか?
→ Yes。法的要件として個人情報開示請求への対応が必要。
選考辞退・落選の理由は採用分析にも使われる。
問い2:監査・履歴・時間軸ロジックが重心か?
→ Yes。「3ヶ月前の選考状況」を再現する要件、
選考通過率の時系列分析、いずれも業務上重要。
問い3:チームに学習コストの余裕があるか?
→ 検討案件。リードエンジニアの理解度と移行戦略次第。
このシリーズでは、ここまでの判断が「Yes」だった前提で先に進む。次章では、Event Sourcing を構成する 5つの必須要素 を見ていく。「Event とは何か」「Stream とは何か」── 用語の定義から、設計判断につながる細部まで降りていく。