要点: Commit済みの業務変更からEventが失われないことが、Event-Driven設計の出発点です。
課題
DB更新とBrokerへのPublishは別操作です。途中停止によりEvent欠落や未CommitデータのEventが発生し、Retryは重複と順序逆転を招きます。
単純な対策だけでは不十分な理由
Brokerは時間とDeployを分離しますが、Application DBとのAtomicityは作りません。Commit後のPublishを当然視すると、欠落が見えなくなります。
Eventは長寿命のInterfaceです。命名、Version、Retention、OwnerをPublic APIと同等に管理します。
処理フロー
Command検証→Database
業務データとOutbox→Relay
Retry付きPublish→Consumer
Idempotentな副作用
アーキテクチャ上の判断
配送前にIntentをCommit
業務更新とEventを一緒に保存し、Relayが確認までPublishをRetryします。
ConsumerをIdempotentにする
処理済みEvent IDまたはInboxを、副作用と同じTransactionで保存します。
Aggregate単位の順序を守る
Aggregate IDでPartitionし、Global Orderを仮定せず古いVersionを拒否します。
さらに深く考える
EventはCommandではなく事実
OrderCreatedはOrder Domainが所有する事実です。SendEmailToCustomerはProducerを他Serviceの実装へ結合します。
互換性をGovernanceする
AdditiveなSchema変更、意味の安定、Contract Testを使い、Retention未承認の機密Fieldは含めません。
実装手順
- EventをRPCではなく、過去形の永続的な事実として定義します。
- Aggregate変更とOutbox行を同一Transactionで保存します。
- 非同期Publishし、全ConsumerでEvent IDによる重複排除を行います。
技術例: 注文作成とEvent発行のAtomic化
BEGIN
INSERT INTO orders (...) VALUES (...)
INSERT INTO outbox(id, aggregate_id, type, payload)
VALUES (:eventId, :orderId, 'OrderCreated.v1', :payload)
COMMIT
relay.publish_unpublished(limit = 500)Broker Ack後のみPublishedにします。Leaseにより別Relayが中断Batchを回収できます。
想定すべき障害パターン
- Publish後、状態更新前にRelayが停止し、Eventが再配送されます。
- Poison EventがPartitionを止めるため、Context付き隔離と制御されたReplayが必要です。
- 非互換Consumerが保持Eventを誤解します。
監視すべきシグナル
| シグナル | 確認する理由 |
|---|---|
| 最古の未Publish Outbox Age | Commit済み処理が下流から見えない時間です。 |
| Event Type別Consumer Lag | 特定業務Flowの遅延を切り分けます。 |
| Duplicate・Replay率 | 不安定なRelayや非Idempotent Consumerを検出します。 |
設計の検証方法
- Commit直後にAPIを停止し、RelayがPublishすることを確認します。
- Ack直後にRelayを停止し、重複配送でも業務効果が一度だけか確認します。
- 次期Consumerで保持Eventを事前Replayします。
安全なRollout計画
低Riskな副作用一つをCompare-onlyで開始します。同期結果と照合後、DB Commit後の応答へ移行します。Replay ToolとOwner Dashboardを整えてからConsumerを増やします。
本番前チェックリスト
- 配送はat-least-onceであり、業務効果のexactly-onceはIdempotencyで実現します。
- Producer Deploy前にSchema互換性を確認します。
- Event AgeとConsumer Lagを利用者影響として監視します。
まとめ
信頼できるEvent-Driven SystemはTransaction境界、Owner、Retry Semanticsが明確です。
