目次を表示する

Event Sourcing 深掘り ── 経緯・必須要素・デファクトツール・実践と落とし穴

Event Sourcing が応える課題 ── CRUD で失われていたもの

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_stagefirst_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_bychanged_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 とは何か」── 用語の定義から、設計判断につながる細部まで降りていく。