まずは、なぜ私たちがイベントソーシングに惹かれたのかを思い出してみましょう:

  • 完全な監査履歴?チェック。
  • 過去の状態を再構築する能力?チェック。
  • ドメインモデルを進化させる柔軟性?チェック。
  • スケーラビリティとパフォーマンスの利点?ダブルチェック。

すべてがあまりにも良すぎるように聞こえました。ネタバレ注意:その通りでした。

セットアップ:私たちの在庫管理システム

私たちのシステムは、複数の倉庫にわたる何百万ものSKUを処理するように設計されていました。イベントソーシングを選んだのは、すべての在庫移動、価格変更、アイテム属性の更新の正確な履歴を維持するためです。イベントストアが真実の源であり、プロジェクションが迅速なクエリのための現在の状態を提供していました。

以下は、私たちのイベント構造の簡略版です:

{
  "eventId": "e123456-7890-abcd-ef12-34567890abcd",
  "eventType": "StockAdded",
  "aggregateId": "SKU123456",
  "timestamp": "2023-04-01T12:00:00Z",
  "data": {
    "quantity": 100,
    "warehouseId": "WH001"
  },
  "version": 1
}

無害に見えますよね?ああ、私たちはなんて無知だったのでしょう。

解明:イベントバージョニングの落とし穴

最初の大きな問題は、StockAddedイベントにreasonフィールドを追加する必要があったときに発生しました。簡単だと思いました。バージョンを上げて、移行戦略を追加するだけです。何が問題になるでしょうか?

すべてが問題になりました。

教訓1:イベントのバージョン管理は命がけで行う

私たちはすべてのイベントに単一のバージョン番号を使用するという古典的なミスを犯しました。これにより、StockAddedを更新したときに、他のすべてのイベントの処理が誤って壊れてしまいました。

私たちがすべきだったことは次のとおりです:

{
  "eventType": "StockAdded",
  "eventVersion": 2,
  "data": {
    "quantity": 100,
    "warehouseId": "WH001",
    "reason": "Initial stock"
  }
}

各イベントタイプを独立してバージョン管理することで、システムを崩壊させたドミノ効果を避けることができました。

教訓2:移行は必須

最初は、イベントハンドラで両方のバージョンを処理するだけで済むと思っていました。大きな間違いでした。システムが成長するにつれて、このアプローチは持続不可能になりました。

代わりに、堅牢な移行戦略を実装すべきでした:


def migrate_stock_added_v1_to_v2(event):
    if event['eventVersion'] == 1:
        event['data']['reason'] = 'Legacy import'
        event['eventVersion'] = 2
    return event

# イベントストアから読み取るときに移行を適用
events = [migrate_stock_added_v1_to_v2(e) for e in read_events()]

スナップショットの物語:最適化が裏目に出るとき

イベントストアが成長するにつれて、プロジェクションの再構築が非常に遅くなりました。そこで登場したのがスナップショットです。私たちの救世主になるはずが、またしても悪夢となりました。

教訓3:スナップショットの頻度は微妙なバランス

最初は100イベントごとにスナップショットを作成しました。これは、トランザクションの急増によりスナップショットの作成が遅れ、プロジェクションがますます古くなってしまうまでうまく機能していました。

解決策は?適応型スナップショット頻度です:


def should_create_snapshot(aggregate):
    time_since_last_snapshot = current_time() - aggregate.last_snapshot_time
    events_since_last_snapshot = aggregate.event_count - aggregate.last_snapshot_event_count
    
    return (time_since_last_snapshot > MAX_TIME_BETWEEN_SNAPSHOTS or
            events_since_last_snapshot > MAX_EVENTS_BETWEEN_SNAPSHOTS)

教訓4:スナップショットにもバージョン管理が必要

スナップショットのバージョン管理を忘れていました。アグリゲート構造を変更したとき、すべてが崩壊しました。古いスナップショットが互換性を失い、プロジェクションを再構築できなくなりました。

修正策は?スナップショットをバージョン管理し、アップグレードパスを提供することです:


def upgrade_snapshot(snapshot):
    if snapshot['version'] == 1:
        snapshot['data']['newField'] = calculate_new_field(snapshot['data'])
        snapshot['version'] = 2
    return snapshot

# スナップショットを読み込むときに使用
snapshot = upgrade_snapshot(load_snapshot(aggregate_id))

腐敗の難問:真実の源が嘘をつくとき

最後の一撃はイベントストアの腐敗でした。ネットワークの問題、イベントストアのバグ、過度に攻撃的なエラーハンドリングが重なり、重複や欠落したイベントが発生しました。

教訓5:信頼するが検証せよ

私たちはイベントストアを盲目的に信頼していました。代わりに、チェックサムと定期的な整合性チェックを実装すべきでした:


def verify_event_integrity(event):
    expected_hash = calculate_hash(event['data'])
    return event['hash'] == expected_hash

def perform_integrity_check():
    for event in read_all_events():
        if not verify_event_integrity(event):
            raise IntegrityError(f"Corrupt event detected: {event['eventId']}")

教訓6:回復戦略を実装せよ

腐敗が発生したとき(そしてそれは必ず起こります)、回復する方法が必要です。私たちはそれを持っておらず、高い代償を払いました。私たちがすべきだったことは次のとおりです:

  1. すべての受信コマンドの別の追加専用ログを維持する。
  2. コマンドログとイベントストアを比較する調整プロセスを実装する。
  3. 欠落したイベントを再生したり、重複を削除したりする回復プロセスを作成する。

def reconcile_events():
    command_log = read_command_log()
    event_store = read_event_store()
    
    for command in command_log:
        if not event_exists_for_command(command, event_store):
            replay_command(command)
    
    for event in event_store:
        if is_duplicate_event(event, event_store):
            remove_duplicate_event(event)

フェニックスの復活:回復力を持って再構築

数え切れないほどの不眠の夜と、言いたくないほどのコーヒーを経て、ついにシステムを安定させました。ここに、私たちが灰から立ち上がるのを助けた重要な教訓があります:

  • イベントのバージョン管理は必須です。最初の日から行いましょう。
  • イベントとスナップショットの両方に対して堅牢な移行戦略を実装しましょう。
  • 適応型スナップショット作成は、パフォーマンスと一貫性のバランスを取ります。
  • 何も信頼せず、すべてのレベルで整合性チェックを実装しましょう。
  • 必要になる前に明確な回復戦略を持ちましょう。
  • 広範なテスト、特にカオスエンジニアリングは、あなたを救うことができます。

結論:イベントソーシングの両刃の剣

イベントソーシングは強力ですが、同時に複雑です。それは万能薬ではなく、実運用で成功するためには慎重な考慮と堅牢なエンジニアリングプラクティスが必要です。

覚えておいてください、大きな力には大きな責任が伴います。そして、イベントソーシングの場合、多くの不眠の夜が伴います。しかし、これらの教訓を持って、あなたはイベントソーシングの野生の挑戦に立ち向かう準備が整いました。

さて、私はイベントソーシングのトラウマを克服するために少し時間が必要です。イベントソーシングのトラウマに特化した良いセラピストを知っている人はいませんか?

"イベントソーシングにおいても人生においても、失敗を避けることではなく、優雅に失敗し、より強く回復することが重要です。"

さらなる読書

あなたもイベントソーシングの悪魔と戦ったことがありますか?コメントであなたの戦争物語を共有してください。苦しみは仲間を愛しますから!