6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

イベント駆動アーキテクチャ(EDA)とは?

6
Posted at

はじめに

ビジネスロジックを処理する関数を見て、「これ、やりすぎじゃないか?」と思ったことはありませんか?データベースへの保存、メール送信、ログ記録、ポイント付与、外部API呼び出し——全部が一か所に詰め込まれている状態です。

これは tight coupling(密結合) のサインです。各部分が強く結びついているため、小さな機能追加でも該当関数を開いて全体を読み直し、「何も壊さなければいいけど…」と祈りながら修正するはめになります。

Event-Driven Architecture(EDA) は、まさにこの問題を解決するためのコード設計アプローチです。

この記事では、概念の説明から動作の仕組み、PHPによる実践例まで順を追って解説します。マイクロサービスや分散システムの知識は必要ありません。


EDAとは何か

イベント駆動アーキテクチャとは、システムの各コンポーネントが互いを直接呼び出す代わりに、イベント(event) を介してやり取りするアーキテクチャパターンです。

イベントとは、「システム内で何かが起きた」という通知です。たとえば:

  • UserRegistered — ユーザーが新規登録した
  • TicketBooked — チケットの予約が完了した
  • PaymentCompleted — 決済処理が完了した

処理の発生元は、後続のすべての処理を直接呼び出す必要はありません。イベントを発行するだけでいい。反応が必要なコンポーネントは自分でそのイベントを購読(subscribe)します。

EDAを構成する3つのコアコンポーネント:

コンポーネント 役割
Event Producer 何かが起きたときにイベントを発行する
Event Bus イベントを受け取り、適切な場所に配信する仲介役
Event Consumer イベントを購読し、独自のビジネスロジックで処理する

イベント名は常に過去形で命名します:TicketBookedUserRegisteredOrderShipped。イベントは「すでに起きたこと」を表すものであり、誰かへの命令ではありません。


EDAを使うべき場面

EDAが適しているのは、次のような状況です:

✅ 1つのアクションが多くの後続処理を引き起こす場合
例:チケット予約 → メール送信 + ポイント付与 + ログ記録 + プッシュ通知。後続処理の数は時間とともに増えていく可能性があります。

✅ 後続処理をリアルタイムで完了させる必要がない場合
メール送信、アナリティクスの記録、レポート更新などは、ユーザーへのレスポンスを返す前に完了させる必要はありません。

✅ 要件が頻繁に変わる場合
今週はバウチャー機能を追加、来週は新しいパートナー連携を追加——そのたびにコアロジックを修正するのはリスクが高すぎます。

✅ 各コンポーネントを独立させたい場合
一部が壊れても全体は止まらない。各部分を独立して開発・テスト・デプロイできるようにしたい。


❌ EDAを使わないほうがいい場面:

  • ロジックがシンプルで変更が少ない場合 — Event Busの層を追加するだけでコードが読みにくくなる
  • 次のステップの結果に依存して処理を続ける必要がある場合 — EDAは互いに依存しないフローに向いている
  • 小規模チームや立ち上げ期のコードベース — まずはシンプルに始め、本当に必要になったらリファクタリングする

メリット

Loose coupling(疎結合)— コンポーネントの分離
ProducerはConsumerが誰で何をするかを知る必要がありません。新しいConsumerを追加しても、ProducerやほかのConsumerに影響はありません。

機能の拡張が容易
新機能を追加する = 新しいConsumerを追加する。既存コードを修正する必要も、現在のロジックを読み返す必要もありません。

コードの可読性と責任の明確化
各Consumerは1つのことだけをする。コアの関数は本来のビジネスロジックだけを処理する。

テストしやすい
システム全体をセットアップしなくても、各Consumerを独立してテストできます。


デメリット

処理の流れが線形でなくなる
通常のコードは上から下へ読めば流れがわかります。EDAでは、どのConsumerがどのイベントを購読しているかを追わなければならず、プロジェクト初参加のメンバーには特に難しく感じられます。

デバッグが難しくなる
エラーが発生した場合、1か所を読むだけでなく、どのConsumerで発生したかを特定する必要があります。

実行順序が保証されない
複数のConsumerが同じイベントを購読している場合、呼び出し順序は登録順に依存します。あるConsumerが別のConsumerの結果に依存していると、微妙なバグを引き起こしやすくなります。

初期の複雑さが増す
Event Bus層の追加、ペイロードの設計、イベント名の命名が必要です。シンプルなシステムでは不要なオーバーヘッドになります。

EDAは特定の問題を解決するためのツールであり、すべてのシステムに適用すべき「黄金律」ではありません。本当に必要なときに使いましょう。「なんとなくかっこいいから」という理由で使ってはいけません。


実践例:PHPで映画チケット予約システムを作る

題材の説明

ユーザーがチケットを予約すると、システムは次のことを行う必要があります:

  1. 確認メールを送信する
  2. ポイントを付与する
  3. アナリティクスを記録する
  4. プッシュ通知を送信する

そして、このリストは今後も増え続けていきます。


従来のやり方(とその問題点)

BookingService_v1.php
class BookingService
{
    public function bookTicket(string $userId, string $movieId, string $seatId): array
    {
        // 予約を保存
        $booking = $this->db->save([
            'user_id'  => $userId,
            'movie_id' => $movieId,
            'seat_id'  => $seatId,
            'status'   => 'confirmed',
        ]);

        // メール送信
        $user = $this->db->getUser($userId);
        $this->emailService->send($user['email'], '予約完了のお知らせ', "座席 $seatId のご予約が確定しました。");

        // ポイント付与
        $this->loyaltyService->addPoints($userId, 10);

        // アナリティクス記録
        $this->analytics->track('ticket_booked', ['movie_id' => $movieId]);

        // プッシュ通知送信
        $this->pushService->send($userId, 'チケットが確定しました!');

        return $booking;
    }
}

この関数は今後どんどん膨らんでいきます。新しい要件が来るたびに開いて修正し、予約ロジックを壊してしまうリスクを負い続けることになります。


Event Busを実装する

EventBus.php
<?php

class EventBus
{
    private array $listeners = [];

    public function subscribe(string $eventName, callable $listener): void
    {
        $this->listeners[$eventName][] = $listener;
    }

    public function publish(string $eventName, array $payload): void
    {
        $handlers = $this->listeners[$eventName] ?? [];

        foreach ($handlers as $handler) {
            try {
                call_user_func($handler, $payload);
            } catch (Throwable $e) {
                // エラーをログに記録するが、他のhandlerの処理は止めない
                error_log("[EventBus] '$eventName' のhandlerでエラー発生: " . $e->getMessage());
            }
        }
    }
}

BookingServiceをリファクタリングする

BookingService_v2.php
<?php

class BookingService
{
    public function __construct(private EventBus $bus) {}

    public function bookTicket(string $userId, string $movieId, string $seatId): array
    {
        // コアロジック:本来の責務だけを担う
        $booking = $this->db->save([
            'user_id'  => $userId,
            'movie_id' => $movieId,
            'seat_id'  => $seatId,
            'status'   => 'confirmed',
        ]);

        $this->db->markSeatUnavailable($seatId);

        // イベントを発行 — 誰が処理するかは関知しない
        $this->bus->publish('TicketBooked', [
            'booking_id' => $booking['id'],
            'user_id'    => $userId,
            'movie_id'   => $movieId,
            'seat_id'    => $seatId,
        ]);

        return $booking;
    }
}

bookTicket() はただ1つのこと、チケットの予約だけを行います。メール、ポイント、アナリティクスが何者かを知る必要はありません。


独立したListenerを実装する

Listeners.php
<?php

// 確認メールを送信
$bus->subscribe('TicketBooked', function (array $payload) use ($emailService, $db) {
    $user = $db->getUser($payload['user_id']);
    $emailService->send(
        $user['email'],
        '予約完了のお知らせ 🎬',
        "座席 {$payload['seat_id']} のご予約が確定しました。映画をお楽しみください!"
    );
});

// ポイントを付与
$bus->subscribe('TicketBooked', function (array $payload) use ($loyaltyService) {
    $loyaltyService->addPoints($payload['user_id'], 10);
});

// アナリティクスを記録
$bus->subscribe('TicketBooked', function (array $payload) use ($analytics) {
    $analytics->track('ticket_booked', [
        'movie_id' => $payload['movie_id'],
        'seat_id'  => $payload['seat_id'],
    ]);
});

// プッシュ通知を送信
$bus->subscribe('TicketBooked', function (array $payload) use ($pushService) {
    $pushService->send($payload['user_id'], 'チケットが確定しました!');
});

新機能の追加:割引バウチャーを配布する

プロダクトオーナーから「次回購入時に使える20%割引バウチャーを配布したい」という要望が来ました。EDAなら、BookingService には手を触れずに対応できます:

VoucherListener.php
<?php

// 新しいListenerを追加するだけ — BookingServiceは一切変更なし
$bus->subscribe('TicketBooked', function (array $payload) use ($voucherService) {
    $voucher = $voucherService->create([
        'user_id'          => $payload['user_id'],
        'discount_percent' => 20,
        'expires_in_days'  => 30,
    ]);

    echo "ユーザー {$payload['user_id']} にバウチャー {$voucher['code']} を発行しました\n";
});

すべてをindex.phpでつなげる

index.php
<?php

require_once 'EventBus.php';
require_once 'BookingService.php';

$bus            = new EventBus();
$bookingService = new BookingService($bus);

// すべてのListenerを登録
require_once 'Listeners.php';
require_once 'VoucherListener.php';

// 動作確認
$result = $bookingService->bookTicket('user_001', 'movie_inception', 'B7');

echo "予約完了: {$result['id']}\n";

実行結果:

ユーザー user_001 に確認メールを送信しました
ユーザー user_001 に 10 ポイントを付与しました
アナリティクスを記録しました: ticket_booked
ユーザー user_001 にプッシュ通知を送信しました
ユーザー user_001 にバウチャー SAVE20_XYZ を発行しました
予約完了: booking_abc

処理の流れをまとめる


実装時の注意点

イベント名は明確かつ一貫したルールで命名する
過去形を使い、起きたことを正確に表現しましょう。TicketBookedBookingDoneHandleBooking より優れています。

ペイロードは「ちょうど十分」な設計にする
ConsumerがそれだけでADditional APIコールなしに処理できる情報を含めましょう。ただし、詰め込みすぎも禁物です。本当に必要なものだけにします。

ConsumerどうしをListener実行順に依存させない
Consumer Bがconsumer Aの結果を必要とするなら、設計を見直すサインです。2つの別々のイベントと処理フローに分割することを検討してください。

Event Busには必ずエラーハンドリングを実装する
あるConsumerでエラーが発生しても、残りのConsumerチェーンを止めてはいけません。上記の EventBus.php の例のように、各handlerをtry/catchで囲み、ログを残しましょう。

Listenerファイルは担当するビジネスロジックの近くに置く
すべてを1つの巨大な listeners.php に詰め込まないようにしましょう。たとえば app/Listeners/Booking/SendConfirmationEmail.php のような構造にすると、見つけやすく、不要になったときも削除しやすくなります。


まとめ

EDAは1つのアクションが多くの後続処理を引き起こすとき、コードが硬直化して保守しにくくなるという、非常に現実的な問題を解決します。

中心的なアイデアはシンプルです——直接呼び出す代わりにイベントを発行し、必要な側が自分で購読する。これにより責務が分離され、既存のコードを壊す心配なく機能を追加できるようになります。

覚えておくべきポイント:

  • イベントは「起きたこと」の通知であり、命令ではない
  • ProducerはConsumerを知らず、ConsumerもProducerを知らない
  • 機能追加 = Listenerの追加、コアコードの修正ではない
  • トレードオフとして、デバッグが難しくなり、処理の流れが追いにくくなる

PHPでは、この記事で紹介したシンプルな EventBus クラスだけで始められます。システムが大きくなり、非同期処理や複数プロセスへの対応が必要になってきたら、LaravelのEvents/Listenersシステムや、Redis Pub/SubといったMessage Brokerの導入を検討しましょう。


参考文献

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?