イベント駆動アーキテクチャ

イベント駆動アーキテクチャ(EDA: Event-Driven Architecture)とは、コンポーネント同士が直接呼び出し合うのではなく、「イベント」の発行と購読を通じて非同期に連携するアーキテクチャパターン。

背景

リクエスト/レスポンス型の連携では、リクエスト側がレスポンス側を直接呼び出し、レスポンスを待つ。この方式には以下の問題がある。

  • リクエスト側がレスポンス側を知っている必要があり、連携先を追加するたびにリクエスト側を変更することになる
  • レスポンス側が遅い、または停止していると、リクエスト側も待たされたり失敗したりして、障害が連鎖する

EDA では、発行側は「何が起きたか」をイベントとして発行するだけで、誰がどう処理するかには関知しない。これにより、コンポーネント間の結合度を下げ、障害の波及を抑えることができる。

構成要素

イベント

システム内で起きた出来事や状態の変化を表す。すでに起きた事実なので、過去形で名付ける。

  • ユーザーが商品を注文した(OrderPlaced)
  • 支払いが完了した(PaymentCompleted)
  • 在庫が更新された(StockUpdated)

イベントは、「〜せよ」という指示(コマンド)とは区別する。コマンドは特定の受け手に処理を依頼するが、イベントは受け手を指定しない。

イベントの形式

イベントの中身は JSON で表すのが一般的。

{
  "type": "OrderPlaced",
  "id": "evt-123",
  "time": "2026-10-09T10:00:00Z",
  "data": {
    "orderId": "order-456",
    "userId": "user-789",
    "totalAmount": 3000
  }
}
  • 大量のイベントを流す環境(Kafka など)では、データが小さく、スキーマを強制できる Avro や Protocol Buffers もよく使われる
  • type や id などの共通項目を定めた標準仕様として CloudEvents がある

リクエスト/レスポンスと異なり、レスポンスにあたるものはない。プロデューサーがブローカーから受け取るのは「受け付けた」という確認だけで、コンシューマーの処理結果は返ってこない。

プロデューサー(発行者)

イベントを発行するコンポーネント。どのコンシューマーが購読しているかを知らない。

コンシューマー(購読者)

イベントを受け取って処理するコンポーネント。同じイベントを、複数のコンシューマーがそれぞれ独立に処理できる。

イベントチャネル

プロデューサーとコンシューマーの間でイベントを仲介する仕組み。チャネルを提供するミドルウェアやサービスを、ブローカーと呼ぶ。主に以下の 2 つの方式がある。

  • メッセージキュー / Pub/Sub: 処理または配信が終わったイベントは基本的に残らない(Amazon SQS、Amazon SNS など)
  • イベントストリーム: イベントをログとして一定期間保持し、コンシューマーは任意の位置から読み直せる(Apache Kafka、Amazon Kinesis など)

補足: Pub/Sub とキュー

Pub/Sub(Publish/Subscribe)は、メッセージの受け渡し方の一般名。Google Cloud の同名のサービス(Google Cloud Pub/Sub)を指す場合もあるので注意する。

  • Pub/Sub 型: 発行側は「トピック」に送るだけで、ブローカーがそのトピックの購読者全員にコピーを配る(例: Amazon SNS)
  • キュー型: メッセージはキューに溜められ、1 つのメッセージを 1 つのコンシューマーだけが処理する(例: Amazon SQS)

Pub/Sub は EDA を実現する代表的な手段だが、両者はイコールではない。EDA はキュー型やイベントストリームでも実現でき、Pub/Sub も EDA 以外(チャットでメッセージを参加者全員に配る仕組みなど)で使われる。

メリットとデメリット

メリット

  • 疎結合: 新しい処理を追加するときは、プロデューサーを変更せずにコンシューマーを足すだけで済む
  • 障害の分離: コンシューマーが停止していてもプロデューサーは処理を続けられ、復旧後に溜まったイベントを処理できる
  • スケーラビリティ: 負荷の高いコンシューマーだけを個別にスケールでき、急な負荷もチャネルで吸収できる(「具体例」を参照)

デメリット

  • 結果整合性: 処理が非同期なので、すべての処理が反映されるまでに時間差が生じる
  • 処理の流れが追いにくい: 呼び出し関係がコードに直接現れないため、全体の流れの把握やデバッグが難しい
  • 配信に関する考慮: 同じイベントが重複して届く、順序が入れ替わる、処理に失敗するといったケースへの対策(冪等性、リトライ、DLQ など)が必要になる

使い分け

リクエスト/レスポンスが向いている場合

  • リクエスト側が結果をその場で必要とする(ログイン認証、画面表示用のデータ取得など)
  • 処理の成否を即座にユーザーへ返す必要がある(決済の承認など)
  • 強い整合性が求められ、途中の状態を見せられない
  • 連携先が少なく、増える見込みもない

EDA が向いている場合

  • 1 つの出来事に対して、複数の処理が後続する(注文確定後のメール送信、配送手配、ポイント付与など)
  • 後続の処理が今後増える見込みがある
  • 後続の処理の完了を待たずに、ユーザーへ応答してよい
  • 負荷の変動が大きく、処理を平準化したい
  • 一部の処理が失敗や停止をしても、全体は止めたくない

実際のシステムでは二者択一ではなく、併用することが多い。たとえば EC サイトでは、注文の受付と決済の承認はリクエスト/レスポンスで行って結果を即座に返し、その後のメール送信や配送手配、分析基盤への連携は「注文が確定した」イベントを起点に非同期で行う。

具体例

EC サイトで、注文の確定後に「メール送信」「ポイント付与」「分析処理」を行うケースを、AWS の SNS と SQS で構成する。

注文 API ── OrderPlaced ──> SNS トピック ─┬─> SQS(メール用)   ──> メール送信
                                          ├─> SQS(ポイント用) ──> ポイント付与
                                          └─> SQS(分析用)     ──> 分析処理

プロデューサー(注文 API)

import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";
 
const sns = new SNSClient({});
 
export async function placeOrder(order: Order) {
  await saveOrder(order);
 
  // 誰が処理するかは知らずに、「注文が確定した」ことだけを発行する
  await sns.send(
    new PublishCommand({
      TopicArn: process.env.ORDER_EVENTS_TOPIC_ARN,
      Message: JSON.stringify({
        type: "OrderPlaced",
        id: crypto.randomUUID(),
        time: new Date().toISOString(),
        data: { orderId: order.id, userId: order.userId, totalAmount: order.totalAmount },
      }),
    }),
  );
 
  // 後続の処理を待たずに応答する
  return { orderId: order.id };
}

コンシューマー(メール送信)

import type { SQSEvent } from "aws-lambda";
 
// メール用の SQS キューにイベントが届くと、Lambda が起動する
export async function handler(event: SQSEvent) {
  for (const record of event.Records) {
    // SNS の raw message delivery を有効にしている前提(無効だと SNS の封筒に包まれて届く)
    const { data } = JSON.parse(record.body);
    await sendOrderConfirmationMail(data.userId, data.orderId);
  }
}

ポイント付与と分析処理も、同じ形のコンシューマーをそれぞれのキューに用意する。

この構成で得られること

  • セールで注文が 10 倍に増え、分析処理だけが追いつかなくなった場合、分析用のコンシューマーだけ同時実行数を増やせばよい
  • 処理しきれないイベントは SQS に溜まって順番待ちになるだけで、注文 API の応答は遅くならない。ピークが過ぎた後に、溜まった分を処理する
  • 新しい処理(例: クーポン発行)を追加するときは、SNS トピックを購読するキューとコンシューマーを足すだけで、注文 API は変更しない

リクエスト/レスポンスで同じことをすると、注文 API が 3 つの処理を順に呼び、すべての完了を待つことになる。分析処理が遅くなると注文 API も遅くなり、ピーク時の量をその場で処理できるだけの台数を、すべての処理に用意する必要がある。

関連パターン

イベントソーシング

状態そのものではなく、状態を変化させたイベントの列を保存し、イベントを順に再生して現在の状態を導出するパターン。EDA がコンポーネント間の連携方式であるのに対し、イベントソーシングはデータの永続化方式であり、EDA を採用してもイベントソーシングは必須ではない。

CQRS

書き込み(Command)と読み取り(Query)のモデルを分離するパターン。書き込み側で発生したイベントを購読して読み取り用のモデルを更新する形で、EDA と組み合わせて使われることが多い。

Saga

サービスごとに DB が分かれていると、複数のサービスにまたがる処理を 1 つのトランザクションにまとめられない。Saga は、こうした処理でデータの整合性を保つためのパターン。

たとえば、注文サービス、決済サービス、在庫サービスがあり、注文の処理が以下の順に進むとする。各ステップは、それぞれのサービスの DB だけを更新する。

  1. 注文サービスが注文を作成する
  2. 決済サービスが支払いを確定する
  3. 在庫サービスが在庫を確保する

3 で在庫が足りなかった場合、1 と 2 はすでに完了しているため、DB のロールバックでは元に戻せない。そこで、完了したステップを打ち消す処理を逆順に実行する。

  • 決済サービスが返金する
  • 注文サービスが注文をキャンセルする

このように、完了したステップを打ち消すための処理を補償トランザクションと呼ぶ。補償トランザクションは自動では用意されないため、ステップごとに設計する必要がある。

Saga の補償トランザクション

各ステップをどうつなぐかには、2 つの方式がある。

  • コレオグラフィー: 各サービスが「注文が作成された」「支払いが確定した」といったイベントを発行し、次のサービスがそれを購読して処理を始める
  • オーケストレーション: 注文の処理全体を管理するコンポーネント(オーケストレーター)が、「支払いを確定せよ」「在庫を確保せよ」と各サービスに順番に指示を出す

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

コレオグラフィーは EDA そのものだが、オーケストレーションはイベントではなく指示(コマンド)で進む。そのため、Saga は EDA の一種ではなく、EDA を使っても使わなくても実現できる。

参考