C4モデルによるイベント駆動アーキテクチャのドキュメンテーション - Archyl Blog

イベント駆動アーキテクチャは強力ですが、ドキュメンテーションが非常に困難なことで知られています。イベントは不可視で、非同期で、設計上疎結合です。このガイドでは、C4モデルを使ってイベントフロー、Event Channel、非同期パターンを文書化する方法、そしてArchylがイベント駆動システムを可視化する方法を解説します。

C4モデルによるイベント駆動アーキテクチャのドキュメンテーション

イベント駆動アーキテクチャは、スケーラブルで疎結合なシステムを構築するためのデフォルトとなっています。サービスは何かが起きた時にイベントをパブリッシュします。他のサービスはそれらのイベントをサブスクライブして反応します。パブリッシャーは誰がリッスンしているか知りません。サブスクライバーは誰がパブリッシュしたか知りません。システムは疎結合で、レジリエントで、柔軟です。

しかし同時に、ほぼ不可視でもあります。

イベント駆動システムの従来のC4図を見ると、メッセージブローカーに矢印が向いたサービスが見えます。サービスAはKafkaにパブリッシュします。サービスBはKafkaからコンシュームします。しかし、どのイベントが間を流れているのか?スキーマは何か?他に誰がリッスンしているのか?コンシューマーが失敗したらどうなるのか?図はプラミングは示しますが、振る舞いを隠しています。

イベント駆動アーキテクチャの文書化には、不可視なものを可視化するテクニックが必要です。インフラ(ブローカー、キュー、トピック)だけでなく、イベント自体、そのフロー、スキーマ、保証を示します。このガイドでは、C4モデルを使用してそれを行う方法と、ArchylのEvent Channel機能がイベント駆動システムをアーキテクチャドキュメンテーションにおけるファーストクラスの市民にする方法を解説します。

イベント駆動システムの文書化が困難な理由

イベント駆動アーキテクチャは、同期的なリクエスト・レスポンスシステムには存在しないドキュメンテーション上の課題をいくつか導入します。

不可視な制御フロー

同期システムでは、呼び出しチェーンをたどることでクライアントからサーバーへのリクエストをトレースできます。サービスAがサービスBを呼び出し、サービスBがサービスCを呼び出します。制御フローは明示的でコードに見えます。

イベント駆動システムでは、制御フローは暗黙的です。サービスAがOrderCreatedイベントをパブリッシュします。どこかでサービスBがそのイベントに反応して在庫を予約します。別のどこかでサービスCが反応して確認メールを送信します。サービスAはBやCについて知りません。制御フローはパブリッシャーのコードではなく、イベントのサブスクリプションによって定義されます。

この間接性は、単一サービスのコードを読むだけではシステムの振る舞いを理解できないことを意味します。サービス間のイベントフローを示す上位レベルのビューが必要です -- それこそがアーキテクチャドキュメンテーションが提供すべきものです。

多対多のリレーションシップ

同期アーキテクチャでは、リレーションシップは通常1対1または1対少数です。サービスAがサービスBを呼び出します。リレーションシップは直接的でAPI呼び出しによって文書化されます。

イベント駆動アーキテクチャでは、リレーションシップは多対多です。単一のイベントタイプに1つのプロデューサーと5つのコンシューマーがいるかもしれません。単一のサービスが10の異なるプロデューサーからイベントをコンシュームするかもしれません。リレーションシップグラフは同期システムよりも密で複雑です。

従来のアーキテクチャ図はこの密度に苦労します。すべてのプロデューサーからすべてのコンシューマーへ、すべてのトピックを通じて矢印を引くと、スパゲッティの皿のような図になります。適切な抽象レベルでイベントフローを示すドキュメンテーションアプローチが必要です。

スキーマの進化

イベントスキーマは時間とともに進化します。OrderCreatedイベントは5つのフィールドで始まり、2年間で15に成長するかもしれません。コンシューマーは特定のフィールドに依存するかもしれません。後方互換性がないスキーマ変更はコンシューマーを壊す可能性があります。

現在のスキーマを文書化するのは必要ですが十分ではありません。スキーマのバージョニング戦略、互換性保証、破壊的変更の履歴も文書化する必要があります。

結果整合性

イベント駆動システムは本質的に結果整合的です。サービスAがイベントをパブリッシュする時、サービスBはミリ秒後に処理するかもしれないし、(コンシューマーが遅れている場合やリトライが必要な場合は)数分後かもしれません。その間、システムは不整合な状態にあります。

ドキュメンテーションはこれらの整合性の境界を捉えるべきです。システムのどの部分が強い一貫性か?どの部分が結果整合性か?期待される伝播遅延は?不整合ウィンドウ中に何が起こるか?

Dead Letter Queueとエラーハンドリング

イベントコンシューマーがメッセージの処理に失敗すると、イベントは通常Dead Letter Queue(DLQ)に送られます。しかしその後どうなるか?誰がDLQを監視するのか?リトライ戦略は?ポイズンメッセージはどう扱われるか?

これらのエラーハンドリングパターンはシステムの振る舞いにとって重要ですが、めったに文書化されません。アーキテクチャの一部であり、ドキュメンテーションで可視化されるべきです。

C4でイベント駆動システムをモデリングする

C4モデルは、各レベルの使い方にいくつかの適応を加えることで、イベント駆動アーキテクチャを効果的に表現できます。

System Context:呼び出しではなくデータフローに焦点を当てる

System Contextレベルでは、イベント駆動アーキテクチャと同期アーキテクチャは似て見えます。システムがユーザーや外部システムと相互作用します。主な違いはリレーションシップのラベル付け方です。

「呼び出す」や「クエリする」の代わりに、データフローを説明するラベルを使用します。

  • 「注文イベントを送信」
  • 「決済確認を受信」
  • 「分析イベントをパブリッシュ」

これらのラベルはハイレベルのビューを実装の詳細で複雑にすることなく、通信の非同期的な性質をほのめかします。

Container図:ブローカーを可視化する

Container図はイベント駆動アーキテクチャが際立つ場所です。メッセージブローカー(Kafka、RabbitMQ、Amazon SQS/SNS、Google Pub/Sub)は、不可視な実装の詳細ではなく、図のファーストクラスコンテナであるべきです。

イベント駆動Eコマースシステムのcontainer図の例:

systems:
  - name: E-Commerce Platform
    type: software_system
    containers:
      - name: Order Service
        type: service
        technologies: [Go, PostgreSQL]
      - name: Inventory Service
        type: service
        technologies: [Java, PostgreSQL]
      - name: Notification Service
        type: service
        technologies: [Python, Redis]
      - name: Analytics Service
        type: service
        technologies: [Python, ClickHouse]
      - name: Event Bus
        type: queue
        technologies: [Apache Kafka]

relationships:
  - from: Order Service
    to: Event Bus
    label: "Publishes OrderCreated, OrderCancelled"
  - from: Event Bus
    to: Inventory Service
    label: "Delivers order events"
  - from: Event Bus
    to: Notification Service
    label: "Delivers order and payment events"
  - from: Event Bus
    to: Analytics Service
    label: "Delivers all domain events"
  - from: Inventory Service
    to: Event Bus
    label: "Publishes InventoryReserved, InventoryReleased"

Event Busが図の中心にあり、リレーションシップがそこを流れるイベントタイプを明示的に命名していることに注目してください。これにより、すべてのプロデューサーとすべてのコンシューマーの間に直接矢印を作ることなく、イベントフローが可視化されます。

Event Channel:ファーストクラスのコンセプト

メッセージブローカー内の個別のトピック、キュー、ストリームには、独自のドキュメンテーションが必要です。各Event Channelにはシステムの理解に重要なプロパティがあります。

  • Channel名: Kafkaトピック、RabbitMQキュー、またはSQSキュー名
  • イベントタイプ: このチャンネルを流れるイベント
  • プロデューサー: このチャンネルにパブリッシュするサービス
  • コンシューマー: このチャンネルからコンシュームするサービス
  • シリアライゼーション: イベントのエンコード方法(JSON、Avro、Protobuf)
  • パーティショニング戦略: イベントがパーティション間にどう分散されるか
  • 保持ポリシー: イベントがどのくらい保持されるか
  • 順序保証: 順序が保持されるか、どの粒度で

Archylでは、Event Channelは専用のエンティティタイプです。Event Channelを作成し、そのプロパティを指定し、プロデュースおよびコンシュームするサービスにリンクします。これにより、イベントフローの構造化されたクエリ可能なモデルが作成されます。

例えば、「orders」Event Channelは以下のように文書化されるかもしれません。

  • 名前: orders
  • ブローカー: Kafka
  • プロデューサー: Order Service
  • コンシューマー: Inventory Service、Notification Service、Analytics Service、Billing Service
  • イベントタイプ: OrderCreated、OrderUpdated、OrderCancelled、OrderCompleted
  • シリアライゼーション: Schema Registry付きAvro
  • パーティショニング: 注文IDによる
  • 保持期間: 7日

このレベルの詳細により、不可視なインフラが可視化され、クエリ可能になります。開発者が注文イベントを誰がコンシュームしているか知る必要がある時、答えは文書化されて検索可能です。

Component図:イベントハンドラーとパブリッシャー

Componentレベルでは、イベント駆動サービスは複雑なサービスについて文書化する価値のある特徴的な内部構造を持ちます。

  • Event Handler: 特定のイベントタイプをコンシュームして処理するコンポーネント
  • Event Publisher: イベントを生成するコンポーネント
  • Saga / Process Manager: イベントを通じたマルチステップワークフローをオーケストレーションするコンポーネント
  • Projection: イベントストリームからリードモデルを構築するコンポーネント

Order ServiceのComponent図は以下を含むかもしれません。

  • Order Controller -- 注文管理のHTTPリクエストを処理
  • Order Processor -- 注文作成と検証のコアビジネスロジック
  • Event Publisher -- OrderCreated、OrderUpdated、OrderCancelledをKafkaにパブリッシュ
  • Payment Event Handler -- PaymentProcessedとPaymentFailedイベントをコンシューム
  • Order Saga -- サービス間の注文フルフィルメントワークフローを管理

サービスがコレオグラフィやオーケストレーションパターンに参加している場合は特に、十分な複雑さがある時にこれらのコンポーネントを文書化してください。

一般的なイベント駆動パターンの文書化

イベント駆動システムには特定のパターンが繰り返し現れます。それらを明示的に文書化することで、チームがコードからパターンをリバースエンジニアリングする手間を省けます。

Event Sourcing

イベントソーシングされたシステムでは、エンティティの状態はスナップショットとして保存されるのではなく、イベントのシーケンスから導出されます。イベントストリームが唯一の真実の源であり、現在の状態はプロジェクションです。

Event Sourcingの文書化方法:

  • どのエンティティがイベントソーシングされているかを特定する
  • 各エンティティのイベントストリーム内のイベントタイプをリストアップする
  • ストリームからリードモデルを導出するプロジェクションを文書化する
  • スナップショッティング戦略(ある場合)を記載する

CQRS(Command Query Responsibility Segregation)

CQRSは書き込み操作(コマンド)と読み取り操作(クエリ)を分離し、しばしばイベントを使用してリードモデルをライトモデルと同期させます。

CQRSの文書化方法:

  • コマンドサイドのコンテナとクエリサイドのコンテナをC4モデルで明確に分離する
  • ライトサイドからリードサイドへのイベントフローを文書化する
  • 整合性モデル(リードモデルがどのくらい遅れ得るか)を記載する

コレオグラフィ vs オーケストレーション

コレオグラフィでは、サービスが独立してイベントに反応します。中央のコーディネーターは存在しません。オーケストレーションでは、中央サービス(オーケストレーターまたはSaga)がコマンドを送信してレスポンスをリッスンすることでワークフローを調整します。

各ワークフローでシステムがどちらのパターンを使用しているかを文書化します。コレオグラフィを使用する場合、期待されるイベントのシーケンスと参加するサービスを文書化します。オーケストレーションを使用する場合、Sagaのステートマシンと発行するコマンドを文書化します。

Dead Letter Queueとリトライパターン

イベント処理のエラーハンドリング戦略を文書化します。

  • どのイベントにDead Letter Queueがあるか?
  • リトライポリシーは(回数、バックオフ戦略)?
  • DLQイベントの監視と再処理の責任者は誰か?
  • DLQの蓄積に対してどのようなアラートがあるか?

Archylでは、DLQをプライマリチャンネルにリンクされた追加のEvent Channelとしてモデル化できます。これにより、エラーハンドリングインフラがアーキテクチャドキュメンテーションで可視化されます。

イベントスキーマの文書化

イベントスキーマはプロデューサーとコンシューマー間のコントラクトです。同期システムのAPIコントラクトと同じレベルのドキュメンテーションに値します。

スキーマドキュメンテーション

各イベントタイプについて以下を文書化します。

  • イベント名: 明確でドメイン固有の名前(OrderCreatedであり、GenericEventではない)
  • スキーマバージョン: スキーマの現在のバージョン
  • フィールド: タイプ、説明、必須か任意かを含むすべてのフィールド
  • ペイロード例: 代表的なJSON/Avro/Protobufの例
  • 互換性: スキーマが前方互換、後方互換、または完全互換かどうか

ArchylのAPI Contract機能を使用して、RESTやgRPCの仕様と並んでイベントスキーマを文書化できます。コントラクトをEvent Channelにリンクして、スキーマとインフラの間に直接の接続を作ります。

スキーマ進化戦略

チームのスキーマ進化へのアプローチを文書化します。

  • スキーマレジストリ(Confluent Schema Registry、AWS Glue)を使用しているか?
  • どの互換性モードが適用されているか(後方、前方、完全)?
  • 破壊的変更はコンシューマーにどう通知されるか?
  • 古いスキーマバージョンの非推奨プロセスは?

この情報はイベントインフラにリンクされたADRに含まれるべきです。システム全体に影響する意思決定であり、一度明確に文書化され、すべてのチームによって参照されるべきです。

イベントフローの可視化

静的な図はイベントインフラを示すことができますが、イベントフロー(ビジネスプロセスを実装するイベントのシーケンス)を示すのは困難です。

ビジネスプロセスにフローを使用する

ArchylのFlow機能により、ビジネスプロセスを実装するイベントのシーケンスを文書化できます。例えば、「注文配置」フローは以下を示すかもしれません。

  1. 顧客がWeb App経由で注文を送信
  2. API GatewayがリクエストをOrder Serviceに転送
  3. Order Serviceが注文を検証して永続化
  4. Order ServiceがOrderCreatedイベントをKafkaにパブリッシュ
  5. Inventory ServiceがOrderCreatedをコンシュームし、在庫を予約
  6. Inventory ServiceがInventoryReservedイベントをパブリッシュ
  7. Payment ServiceがInventoryReservedをコンシュームし、決済を処理
  8. Payment ServiceがPaymentProcessedイベントをパブリッシュ
  9. Notification ServiceがPaymentProcessedをコンシュームし、確認メールを送信

このフローは、イベント駆動アーキテクチャから生まれるエンドツーエンドの振る舞いを示します。単一サービスのコードはこのフローを明らかにしません。アーキテクチャレベルにのみ存在します。

異なるビューにオーバーレイを使用する

イベント駆動アーキテクチャの異なる側面を示すオーバーレイを作成します。

  • Event Flowオーバーレイ: 同期通信を隠し、イベント関連のリレーションシップのみを強調表示
  • Producer/Consumerオーバーレイ: サービスがプロデュース、コンシューム、またはその両方かで色分け
  • Error Handlingオーバーレイ: DLQ、リトライパス、モニタリングを表示

オーバーレイにより、単一のアーキテクチャモデルからフォーカスされたビューを作成でき、複数の冗長な図を作る必要がなくなります。

ベストプラクティス

技術的操作ではなくドメインアクションにちなんでイベントを命名する

OrderCreatedPaymentFailedInventoryReserved のような名前を使用します -- DataUpdatedMessageSentRecordInserted ではなく。ドメイン固有の名前はアーキテクチャレベルでイベントフローを読みやすくします。

「なぜ同期ではないか」の意思決定を文書化する

各イベント駆動のインタラクションには、同期ではなく非同期を使用する意思決定がありました。その決定を文書化してください。なぜOrder ServiceはInventory Serviceを直接呼び出す代わりにイベントをパブリッシュするのか?答え(疎結合、レジリエンス、スケーラビリティ)はADRに記録されるべきです。

Event Channelドキュメンテーションをコードの近くに保つ

イベントスキーマがコードで定義されている場合(Protobufファイル、Avroスキーマ、JSON Schema)、アーキテクチャドキュメンテーションからそれらのファイルにリンクしてください。これにより、抽象的なドキュメンテーションと具体的な実装の間に接続が作られます。

アーキテクチャレビューでイベントフローをレビューする

四半期ごとのアーキテクチャレビューで、文書化されたイベントフローを確認します。以下を聞いてください。

  • 文書化されていない新しいイベントタイプはないか?
  • もはや使用されていない文書化されたイベントタイプはないか?
  • ドキュメンテーションの更新なしに新しいコンシューマーが追加されていないか?
  • 文書化されたスキーマはまだ正確か?

Archylのドリフト検出はこれらの質問に自動的に答えるのに役立ちますが、定期的な人間のレビューは自動チェックが見逃すものをキャッチします。

まとめ

イベント駆動アーキテクチャのドキュメンテーションは、不可視なものを可視化するための意図的な努力を必要とします。イベントは設計上、プロデューサーをコンシューマーから分離します。これはアーキテクチャ上の強みですが、ドキュメンテーション上の課題です。

C4モデルがフレームワークを提供します。全体像のためのSystem Context、メッセージブローカーをファーストクラス要素としたContainer図、詳細なトピックとキューのドキュメンテーションのためのEvent Channel、複雑なイベントハンドラーとSagaのためのComponent図です。

Archylがツールを提供します。ファーストクラスエンティティとしてのEvent Channel、イベント駆動ビジネスプロセスを文書化するためのフロー、イベントスキーマのためのAPIコントラクト、フォーカスされたビューのためのオーバーレイ、文書化されていない変更をキャッチするドリフト検出です。

イベント駆動アーキテクチャを設計するのと同じ方法で文書化してください。意図を持ち、構造を持ち、システムの振る舞いは個々のサービスからではなく、サービス間のインタラクションから生まれるという理解を持って。

Archylで始めましょう。イベント駆動アーキテクチャを可視化し、文書化し、理解されるものにしましょう。