イベントソーシング

状態の変更をイベントとして記録し、イベントの再生で現在の状態を復元する設計パターン

設計パターンイベント駆動
イベントソーシング」の技術書を見る →

イベントソーシングとは

イベントソーシングは、状態の変更をイベントとして記録し、イベントの再生 (リプレイ) で現在の状態を復元する設計パターンである。CRUD の「更新」ではなく「追記」でデータを管理する。

CRUD vs イベントソーシング

CRUD とイベントソーシングの違いを図で示す。

CRUD:
  UPDATE accounts SET balance = 900 WHERE id = 'A';
   以前の値 (1000) は失われる

イベントソーシング:
  Event 1: AccountCreated { id: 'A', balance: 1000 }
  Event 2: MoneyWithdrawn { id: 'A', amount: 100 }
   現在の状態: balance = 1000 - 100 = 900
   全履歴が残る

DynamoDB でのイベントストア

DynamoDB でのイベントストアのコード例を示す。

// イベントの書き込み (entityId = パーティションキー / version = ソートキー)
await db.put({
  TableName: 'events',
  Item: {
    entityId: 'order-123',
    version: 1,              // 集約ごとの連番。並び順の根拠はこれ
    occurredAt: Date.now(),  // 記録用。並び順には使わない
    eventType: 'OrderCreated',
    data: { items: ['item-1'], total: 1000 },
  },
  // 同じ (entityId, version) が既にあれば失敗する = 追記の楽観的排他
  ConditionExpression: 'attribute_not_exists(entityId)',
});

// 状態の復元 (イベントのリプレイ)
const events = await db.query({
  TableName: 'events',
  KeyConditionExpression: 'entityId = :id',
  ExpressionAttributeValues: { ':id': 'order-123' },
  ScanIndexForward: true, // ソートキー (version) 昇順 = 発生順
});

const state = events.Items.reduce((order, event) => {
  switch (event.eventType) {
    case 'OrderCreated': return { ...event.data, status: 'created' };
    case 'OrderPaid': return { ...order, status: 'paid' };
    case 'OrderShipped': return { ...order, status: 'shipped' };
    default: return order;
  }
}, {});

メリット

全変更履歴がイベントとして残るため、完全な監査証跡が得られる。任意の時点の状態を復元する「時間旅行」が可能で、デバッグ時に問題の再現が容易になる。イベントが発生するたびに下流システムへ自然に通知できるため、イベント駆動アーキテクチャとの親和性が高い。

デメリット

代表的なデメリットと、その定石の対策を以下にまとめる。

デメリット対策
クエリが複雑CQRS で読み取り用ビューを分離
イベント数の増加スナップショットで高速化
スキーマの進化イベントのバージョニング

表に載りにくい最大の落とし穴は、リプレイと外部システムの相性である。Martin Fowler は原典で、イベント再生時に外部システムへ更新メッセージを送ると、外部システム側は本番処理と再生を区別できないため処理が壊れると指摘している。原典の対処は、外部システムをゲートウェイで包み、リプレイ中はゲートウェイを無効化しておくことである。無効化はドメインロジックから見えないようにする (PaymentGateway.send の呼び方はリプレイ中も変えない)。

逆向きの問題もある。外部システムへの問い合わせ結果がイベント処理の結果を左右する場合、12 月 5 日のイベントを 12 月 20 日に再生すると、必要なのは 12 月 5 日時点の為替レートで、再生時点のレートではない。過去日付を指定して取得できる外部システムなら任せられるが、そうでなければ問い合わせて得た値をイベント自身に記録し、再生では外部へ問い合わせない設計にしておく。この見極めを設計時に飛ばすと、「いつでも再生できる」という前提が後から崩れる。

CQRS との組み合わせ

CQRS との組み合わせを図で示す。

書き込み: イベントストア (DynamoDB)
  ↓ DynamoDB Streams
読み取り: マテリアライズドビュー (別テーブル or OpenSearch)

ユースケース

代表的なユースケースを以下にまとめる。

ユースケース理由
金融取引監査証跡が必須
注文管理状態遷移の履歴が重要
ゲームリプレイ機能
コラボレーション変更の競合解決

全体像を把握するには関連書籍も有用。

この記事は役に立ちましたか?

関連用語

関連する記事