https://a.storyblok.com/f/270183/1368x665/a1e36438ef/25jul_dev_blog_event-sourcing.png

イベント・ソースにすべきか?

最終更新日 July 17, 2025

所要時間:1 分

2020年代がハイテク分野で何かを示したとすれば、「ブロックチェーン」という言葉は、肯定的な解釈も否定的な解釈も無数に存在することになる。ブロックチェーンの "良い "面や "有用な "面を挙げるとすれば、それは多くの場合、その不変的な性質の利点を活用することと結びついている。歴史は歴史である。

しかし、部屋の中の象も認めなければならない:ウェブ3、ブロックチェーン、暗号技術は統合するのが難しい技術だ。もしあなたがソフトウェアエンジニアリングの伝統的なアプローチから来たのなら、朗報だ!データの過去の状態をトレースするアプローチと似たようなデータ設計のパターンが、かなり以前から(特に金融などの分野で)存在しているのだ。この記事では、次のようなものを探ります。 イベント・ソーシング。

イベント・ソーシングの起源

Image showing generic line chart data being mappedAll Together Now: Data is the new Oilイベント・ソーシングというコンセプトは、データをトランザクション・ログに記録する必要性と、時間の経過とともにデータにどのような変化が起こるかというニーズから生まれた。銀行のような機関は、あるエンティティが正確な時間にどのような状態であったかの "スナップショット "を見ることができる必要がある。Domain-Driven-Design (DDD)の概念を用いて、Event Sourcingのより正式な誕生は、以下の発表と共に起こった。 コマンド・クエリ責任分離 (またはCQRS)の立ち上げによって、より正式にイベント・ソーシングが誕生した。この分離は、データではなく、その状態が変化したときに起こるアクションに焦点を当てている。これらのアクションは不変のエンティティとして記録され、Event Sourcingを定義するのはこのパターンである。

金融における典型的なアプリケーション

Photo of paperwork showing generic stock market press reportingIt's a Bull Market, but how do you audit it?たとえば、口座残高のような単純なものを考えてみよう。伝統的なセットアップでは、リレーショナルデータベースのようなものがあり、アプリケーションは準備されたSQLステートメントでそのAccountからデータを取り出します。残高の数値は、トランザクションが各トランザクションの状態でAccountの残高を保持する場合、またはアプリケーションがすべてを集約しようとする場合のいずれかで取得することができます。何百万行もあって、過去の正確な時刻の残高の状態を見たい場合はどうすればいいのでしょうか?トランザクションデータがすべて が正しいことを期待しなければならない、 そして、トランザクションレコードの残高合計を表示しようとしたまさにその時点で、ダウンストリームへの移行が起こっていなかったり、障害が発生していなかったりすることを期待しなければならない。

Event Sourcingでは、保持されるデータがデータの状態の変化に関するものであるため、「履歴を再生」することになる。そのため、これを適用するには、以下のような適切な名前のカスタム・イベント・モデルを作成します:

class AccountCreatedEvent extends Event

class AccountDepositEvent extends Event

class AccountWithdrawEvent extends Event

この一連の出来事を ストリームと呼ばれる。 バランスを取る方法は、アグリゲータを使うことである。 a プロジェクター. プロジェクター・クラスは引数を取り、そこに渡されたイベント(Account IDのようなプライマリ・キーで事前に照会されたもの)を読み込んで、状態を計算する:

class AccountBalanceProjector {

    public static int projectBalance(List<Event> events) {

        int balance = 0;
        boolean accountCreated = false;

        for (Event event : events) {
            if (event instanceof AccountCreatedEvent) {
                balance = ((AccountCreatedEvent) event).initialBalance;
                accountCreated = true;
            } else if (accountCreated && event instanceof AccountDepositEvent) {
                balance += ((AccountDepositEvent) event).amount;
            } else if (accountCreated && event instanceof AccountWithdrawEvent) {
                balance -= ((AccountWithdrawEvent) event).amount;
            }
        }
        return balance;
    }
}

これで完成だ。オブジェクトリレーショナルマッパー(ORM)のメソッドで、特定の accountIdにそれらのイベントを渡します。 AccountBalanceProjector.projectBalance(events)そして残高が戻ってくる。あらかじめどこかで作成された残高を取り出すのではなく、再作成するのです。

Vonageデータでイベント・ソーシングを利用する

Graphic design image showing developers hard at workControlling Data Can Be TrickyVonageを使用する際に生成されるデータを使った例を見てみよう。Voice APIは、音声通話中にアクションが発生するたびにイベントステータスWebhookを生成します。によると Voice API Webhooks ドキュメントによるとによると、以下のステータスを与えることができる:

これらの状態をAIに入力し、プロンプトにクラスを生成するよう依頼すれば、ちょっとした魔法のような自動スプリンクルがイベントのリストを生成してくれる:

public class CallStarted extends BaseVoiceEvent {
    public CallStarted(String callId) { super(callId); }

    @Override public String getName() { return "started"; }
}

public class CallRinging extends BaseVoiceEvent {
    public CallRinging(String callId) { super(callId); }

    @Override public String getName() { return "ringing"; }
}

public class CallAnswered extends BaseVoiceEvent {
    private String answerType;

    public CallAnswered(String callId, String answerType) {
        super(callId);

        this.answerType = answerType;
    }

    @Override public String getName() { return "answered"; }

    @Override public Map<String, Object> toMap() {
        Map<String, Object> map = super.toMap();
        map.put("answer_type", answerType);
        return map;
    }
}

public class CallBusy extends BaseVoiceEvent {
    public CallBusy(String callId) { super(callId); }

    @Override public String getName() { return "busy"; }
}

public class CallCancelled extends BaseVoiceEvent {
    public CallCancelled(String callId) { super(callId); }

    @Override public String getName() { return "cancelled"; }
}

public class CallUnanswered extends BaseVoiceEvent {
    public CallUnanswered(String callId) { super(callId); }

    @Override public String getName() { return "unanswered"; }
}

public class CallDisconnected extends BaseVoiceEvent {
    public CallDisconnected(String callId) { super(callId); }

    @Override public String getName() { return "disconnected"; }
}

public class CallRejected extends BaseVoiceEvent {
    public CallRejected(String callId) { super(callId); }

    @Override public String getName() { return "rejected"; }
}

public class CallFailed extends BaseVoiceEvent {
    public CallFailed(String callId) { super(callId); }

    @Override public String getName() { return "failed"; }
}

public class CallHumanMachineDetected extends BaseVoiceEvent {
    private String detectionResult;

    public CallHumanMachineDetected(String callId, String detectionResult) {
        super(callId);

        this.detectionResult = detectionResult;
    }

    @Override public String getName() { return "human_machine"; }

    @Override public Map<String, Object> toMap() {
        Map<String, Object> map = super.toMap();
        map.put("detection_result", detectionResult);

        return map;
    }
}

public class CallTimeout extends BaseVoiceEvent {
    public CallTimeout(String callId) { super(callId); }

    @Override public String getName() { return "timeout"; }
}

public class CallCompleted extends BaseVoiceEvent {
    public CallCompleted(String callId) { super(callId); }

    @Override public String getName() { return "completed"; }
}

// 等々、長さの都合で切り捨て

これらのEventは、抽象クラスとして定義したベース・クラスを継承していることがわかるだろう:

import java.time.Instant;

import java.util.HashMap;

import java.util.Map;

public abstract class BaseVoiceEvent {
    protected String callId;
    protected Instant timestamp;

    public BaseVoiceEvent(String callId) {
        this.callId = callId;

        this.timestamp = Instant.now();
    }

    public BaseVoiceEvent(String callId, Instant timestamp) {
        this.callId = callId;

        this.timestamp = timestamp;
    }

    public abstract String getName();

    public Map<String, Object> toMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("event", getName());
        map.put("call_id", callId);
        map.put("timestamp", timestamp.toString());

        return map;
    }
}

これで、イベント・ソーシング・アーキテクチャの最初の構造ができた。最後のステップは、アプリケーションへのHTTPエンドポイントでイベント生成を処理するコントローラと、コールステータス・プロジェクタを持つことだ。このプロジェクターは、前に説明した例と同様に、イベントのストリームを受け取り、オプショ ンのタイムスタンプを受け取り、callIDの正しいステータスを、指定したタイムスタンプ(ま たはデフォルトで最新のもの)に変換する。

イベント・ソーシングのメリットとは?

  • イベント駆動型アーキテクチャはリファクタリングが可能だ。システムは入ってくるステートデータから切り離されているため、新しいコントローラを新しいロジックで再実行し(例えば、新しいイベントタイプを処理するため)、投影用のデータストリームを再構築することができる。

  • 時間を問わない完全な監査証跡これは、金融やヘルスケアに見られるように、規制の厳しい業界にとっては非常に重要なことです。

  • リードとライトの分離: 前述のCQRSを使用すると、データのアクセス方法と書き込み方法が分割されます。高いデータの入出力を必要とするアプリケーションをチューニングする場合、それに応じてアーキテクチャの別々の側面をスケールすることができます。

イベント・ソーシングのデメリットとは?

その欠点はかなり大きい。

  • 後方互換性: 新しい変更をモデルにプッシュし、イベントハンドラを再実行することがいかに迅速であるかについては述べた。イベントハンドリング、セマンティックバージョニング、ビヘイビアを考慮する必要がある。

  • システムの複雑さ: これが克服すべき最大のハードルだ。私は過去にスケールアップするイベント・ドリブン・システムに携わったことがあるが、この複雑さの最大の部分はマイクロサービスと似ている。イベント・システムを最初から正しく設計していなければ 最初から もし最初からイベント・システムを正しく設計していないのであれば、変化する列挙型がシステム全体をつまずかせるのを待っていると断言できる。

  • デバッグ ソフトウェア設計において、私が感じる最大のフラストレーションのひとつは、適切なデバッグツールが利用できない場合である。私はしばしば について話したことがある。PHPスタックの一部としてDerick Rethanのxdebugがないことに落胆している。ランタイムを殺して個々の配列や文字列をダンプするのではなく、これなしでコーディングできる人の気が知れない。同様に、マイクロサービスのデバッグ、そしてイベント・ソーシング環境の話につながるが、ストリームを正確にデバッグするためには、おそらく、うまくいかないときのために、システムの上にカスタム・ツールを追加で書く必要があるだろう。

  • ストレージ:これは少しわかりやすいかもしれないが、真実のソースが1つ(この例ではVonage Voice API Webhooksを保存していた)ではなく、以下のような余分なテーブルがいくつかある。 多くの の追加データがソースデータの上に作成される。

自分のやり方で

これまで見てきたところでは、これを行うためのバニラ構造はかなり複雑だ。良いニュースは、それを実装するためのライブラリが存在することだ。ここでは、バックエンド言語ごとに私が推奨するものを紹介する:

結論

新しいエンジニアリング・パターンが登場するたびにそうだが、こうしたものが実際にどれほど役に立つのか、私はしばしば皮肉な気持ちになる。六角形アーキテクチャや逆相続パターンReactフロントエンド・アプリケーションの話を見たことがあるが、開発者がなぜ 私には理解できない。 開発者がなぜこのレベルの複雑さを求めるのか、私には理解できない。しかし、イベント・ソーシングの場合は、状況が全く違うと感じる。確かに実装やアーキテクトは難しいが、何事もそうであるように、あなたのユースケースは何だろうか?もし、あなたが高度なデータ精査を必要とする垂直市場で記事を書いているのであれば、最初からEvent Sourcingを導入することで、将来大きな頭痛の種になる可能性からあなたのビジネスを守ることができるでしょう。

ご質問がある場合、またはあなたが作っているものを共有したい場合は、こちらをクリックしてください。

最新の開発者向けニュース、ヒント、イベント情報をお届けします。

シェア:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeシニアPHPデベロッパー

スタンダップ・コメディーの学位論文を持つ俳優の訓練を受け、ミートアップ・シーンを経てPHP開発に携わるようになった。技術について話したり書いたり、レコード・コレクションから変わったレコードを再生したり買ったりしています。