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

ドメインイベント

アプリケーションが成長すると、1つの処理にいろいろな後続処理がくっついてきます。ログイン処理なのに統計更新、メール通知、監査ログ記録まで全部知っている...。これを解決するのが「ドメインイベント」です。

「何が起きたか」を表すオブジェクト

ドメインイベントとは、システム内で発生したビジネス上意味のある出来事を表すオブジェクトです。

┌─────────────────────────────────────────────────────────────┐
│ ドメインイベントの例 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 認証システム: │
│ ├── UserRegistered(userId, email, timestamp) │
│ ├── LoginSucceeded(userId, ipAddress, timestamp) │
│ ├── LoginFailed(userId, reason, timestamp) │
│ ├── PasswordChanged(userId, timestamp) │
│ └── TokenIssued(userId, clientId, scopes, timestamp) │
│ │
│ ECサイト: │
│ ├── OrderPlaced(orderId, items, total, timestamp) │
│ ├── PaymentCompleted(orderId, amount, timestamp) │
│ └── OrderShipped(orderId, trackingNumber, timestamp) │
│ │
│ 共通する特徴: │
│ ・過去形(〜した) │
│ ・変更不可(起きた事実は変えられない) │
│ ・いつ起きたか(タイムスタンプ)を持つ │
│ │
└─────────────────────────────────────────────────────────────┘

なぜイベントとして表現するのか

Before: 状態の直接変更

┌─────────────────────────────────────────────────────────────┐
│ ログイン処理 │
│ │
│ 1. パスワード検証 │
│ 2. セッション作成 │
│ 3. ログインカウント +1 ← DB UPDATE │
│ 4. 最終ログイン時刻更新 ← DB UPDATE │
│ 5. 統計テーブル更新 ← DB UPDATE │
│ 6. メール通知送信 ← HTTP 通信 │
│ 7. Slack通知 ← HTTP 通信 │
│ 8. 監査ログ記録 ← DB INSERT │
│ │
│ → ログイン処理が通知・統計・監査の詳細を全部知っている │
│ → 新しい通知先が増えるたびにログイン処理を修正 │
│ → 処理が長くなる、テストしにくい、障害が波及する │
└─────────────────────────────────────────────────────────────┘

After: イベントの発行

┌─────────────────────────────────────────────────────────────┐
│ ログイン処理 │
│ │
│ 1. パスワード検証 │
│ 2. セッション作成 │
│ 3. イベント発行: LoginSucceeded(userId, timestamp) │
│ → 完了。ログイン処理はここまで │
│ │
│ イベントの購読者(独立して動作): │
│ ├── 統計更新サービス → 統計テーブル更新 │
│ ├── 通知サービス → メール/Slack 送信 │
│ ├── 監査ログサービス → 監査ログ記録 │
│ └── 分析サービス → ClickHouse に送信 │
│ │
│ → ログイン処理は通知・統計の存在を知らない │
│ → 新しい購読者を追加してもログイン処理は変更不要 │
│ → 各サービスが独立してテスト・デプロイ可能 │
└─────────────────────────────────────────────────────────────┘

このパターンは Pub/Sub(Publish/Subscribe) として広く知られています。ドメインイベントは DDD の用語ですが、仕組みは Pub/Sub そのものです。

文脈発行側仲介購読側
デザインパターンPublisherEvent ChannelSubscriber
DDDドメインイベント発行Event Busイベントハンドラ
SpringApplicationEventPublisherSpring Event@EventListener
KafkaProducerTopicConsumer
AWSPublisherSNS / EventBridgeSubscriber / Lambda

名前は違いますが、本質は同じ ── 発行者は購読者を知らない、購読者は好きなイベントを購読する


イベントの3つの性質

1. 不変(Immutable)

起きた事実は変えられません。「ログインした」は取り消せません。

❌ event.setTimestamp(newTime)   // イベントは変更できない
✅ 新しいイベントを発行する // 「パスワードリセットした」

2. 過去形(Past Tense)

イベントは「起きたこと」なので、命名は過去形。

❌ Login, CreateUser, SendEmail      // コマンド(指示)
✅ LoginSucceeded, UserCreated, EmailSent // イベント(事実)

3. 自己完結(Self-contained)

イベントを見るだけで何が起きたかわかる。他のデータを参照する必要がない。

❌ LoginEvent(userId: 123)
→ 何が起きた? 成功? 失敗? いつ?

✅ LoginSucceeded(
userId: 123,
tenantId: "tenant-A",
ipAddress: "1.2.3.4",
timestamp: "2026-03-25T09:00:00Z"
)
→ これだけで完全にわかる

イベントの活用パターン

パターン1: 通知(Fire and Forget)

イベント発行 ──→ 購読者が非同期で処理
├── メール送信
├── Slack通知
└── Webhook呼び出し

特徴: 発行者は結果を気にしない
用途: 通知、ログ記録

パターン2: 状態更新(Event-Carried State Transfer)

イベント発行 ──→ 購読者が自分のデータストアを更新
├── 統計テーブル更新
├── 検索インデックス更新
└── キャッシュ更新

特徴: 各サービスが自分用のデータコピーを持つ
用途: CQRS の読み取りモデル構築

パターン3: イベント記録(Event Sourcing)

イベント発行 ──→ イベントストアに永続化
→ 現在の状態はイベントの再生で導出

特徴: イベント自体がデータの本体
用途: 監査証跡、時間旅行、デバッグ
→ 次のドキュメントで詳しく解説

よくある疑問

Q: イベントはどこに保存する?

方法適用場面
同じDB内のテーブルevents テーブルに INSERTシンプル、トランザクション保証
メッセージキューKafka, RabbitMQマイクロサービス間の伝搬
専用イベントストアEventStoreDBイベントソーシング専用

Q: イベントの購読者が失敗したら?

購読者の失敗はイベント発行者に影響しません(疎結合)。購読者側でリトライやデッドレターキューで対処します。

イベント発行 ──→ メール送信失敗
→ リトライキューに入れて後で再送
→ 認証処理自体は成功のまま

Q: イベントの順序は保証される?

同一エンティティのイベント順序は保証すべきです(同じユーザーのログイン→ログアウトの順序)。異なるエンティティ間の順序は通常保証しません。


まとめ

┌────────────────────────────────────────────────────────────┐
│ ドメインイベントの価値 │
├────────────────────────────────────────────────────────────┤
│ │
│ 1. 疎結合: 発行者は購読者の存在を知らない │
│ 2. 拡張性: 新しい購読者を追加しても既存コードは変更不要 │
│ 3. 監査性: 何が起きたかの記録が自然に残る │
│ 4. 再利用: 同じイベントを複数の目的で活用できる │
│ │
└────────────────────────────────────────────────────────────┘

次のステップ