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

Sollte ich Event Source?

Zuletzt aktualisiert am July 17, 2025

Lesedauer: 7 Minuten

Wenn die 2020er Jahre irgendetwas im Bereich der Technik gezeigt haben, dann ist es das Wort "Blockchain", das in einer Vielzahl von positiven und negativen Interpretationen auftaucht. Nimmt man die "guten" oder "nützlichen" Aspekte einer Blockchain, so stehen sie oft im Zusammenhang mit der Nutzung der Vorteile ihrer unveränderlichen Natur. Geschichte ist Geschichte.

Sie haben also eine Idee und denken "Blockchain wäre perfekt dafür", aber Sie müssen auch den Elefanten im Raum anerkennen: web3, Blockchains und Kryptotechnologie sind schwierig zu integrierende Technologien. Wenn Sie von einem traditionellen Ansatz in der Softwareentwicklung kommen, gibt es gute Neuigkeiten! Es gibt ein Muster des Datendesigns, das es schon seit geraumer Zeit gibt (insbesondere in Bereichen wie dem Finanzwesen), das einen ähnlichen Ansatz für die Nachverfolgung des historischen Zustands von Daten verfolgt. In diesem Artikel werden wir Folgendes untersuchen Ereignis-Beschaffung.

Die Ursprünge des Event Sourcing

Image showing generic line chart data being mappedAll Together Now: Data is the new OilDas Konzept des Event Sourcing entstand aus der Notwendigkeit, Daten transaktional zu protokollieren und festzustellen, welche Veränderungen an den Daten im Laufe der Zeit vorgenommen werden. Institutionen wie Banken mussten in der Lage sein, einen "Schnappschuss" davon zu erhalten, wie eine Art von Einheit zu einem bestimmten Zeitpunkt aussah. Unter Verwendung des Konzepts des Domain-Driven-Design (DDD) erfolgte die formale Geburt des Event Sourcing mit der Einführung von Command Query Responsibility Segregation (oder CQRS), die üblicherweise in Microsoft-basierten Umgebungen verwendet wird. Diese Segregation konzentriert sich nicht auf Daten, sondern auf die Aktionen, die bei Zustandsänderungen stattfinden. Diese Aktionen werden als unveränderliche Entitäten aufgezeichnet, und dieses Muster ist es, das Event Sourcing definiert.

Eine typische Anwendung im Finanzwesen

Photo of paperwork showing generic stock market press reportingIt's a Bull Market, but how do you audit it?Nehmen Sie zum Beispiel etwas Einfaches wie einen Kontostand. In einer herkömmlichen Konfiguration hätten Sie eine relationale Datenbank und eine Anwendung, die mit einer vorbereiteten SQL-Anweisung Daten aus diesem Konto abruft. Den Kontostand erhält man entweder, wenn Transaktionen den Kontostand bei jeder Transaktion im Status halten, oder wenn die Anwendung versucht, alles zu aggregieren. Hier kommt das Event Sourcing ins Spiel: Was ist, wenn Sie Millionen von Zeilen haben und den Status eines Saldos zu einem bestimmten Zeitpunkt in der Vergangenheit abfragen wollen? Sie müssen hoffen, dass die Transaktionsdaten alle gerade richtig sind, und dass nicht genau zu dem Zeitpunkt, an dem Sie versuchen, einen Gesamtsaldo in einen Transaktionsdatensatz zu übertragen, eine Migration nach unten stattgefunden hat oder ein Ausfall aufgetreten ist.

Beim Event Sourcing wird die "Historie" wiedergegeben, da es sich bei den gespeicherten Daten um die Änderungen des Datenzustands handelt. Um dies anzuwenden, müssen Sie also einige benutzerdefinierte Ereignismodelle erstellen, die entsprechend benannt sind, wie z. B.:

class AccountCreatedEvent extends Event

class AccountDepositEvent extends Event

class AccountWithdrawEvent extends Event

Diese Reihe von Ereignissen wird als ein Strom. Der Weg, um den Saldo herauszubekommen, ist mit einem Aggregator, der als a Projektor. Eine Projektorklasse nimmt ein Argument entgegen und liest dann die Ereignisse ein (die zuvor durch einen Primärschlüssel wie die Account-ID abgefragt wurden), die ihr übergeben werden, um den Status zu ermitteln:

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;
    }
}

Und da haben wir es. Sie hätten eine Methode in Ihrem objektrelationalen Mapper (ORM), die alle Ereignisse für ein bestimmtes Ereignis abrufen würde. accountIdabruft, dann diese Ereignisse an AccountBalanceProjector.projectBalance(events)und zurück kommt Ihr Saldo. Es wird nicht der Saldo herausgezogen, der irgendwo vorher erstellt wurde, sondern er wird neu erstellt.

Verwendung von Event Sourcing mit Vonage-Daten

Graphic design image showing developers hard at workControlling Data Can Be TrickyBetrachten wir ein Beispiel mit Daten, die bei der Verwendung von Vonage generiert werden. Wir werden uns die Voice API ansehen, die jedes Mal einen Ereignisstatus-Webhook erzeugt, wenn eine Aktion während eines Sprachanrufs stattfindet. Laut der der Voice API Webhooks-Dokumentationkönnen die folgenden Zustände angegeben werden:

Wenn ich diese Zustände in die KI einspeise und die Eingabeaufforderung auffordere, meine Klassen zu generieren, kann ich eine kleine magische automatische Berieselung erhalten, um meine Liste von Ereignissen zu generieren:

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"; }
}

// usw. usw., aus Gründen der Länge gekürzt

Sie werden feststellen, dass diese Ereignisse von einer Basisklasse abgeleitet sind, die wir als abstrakt definieren:

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;
    }
}

Sie haben nun die Grundstruktur einer Event-Sourcing-Architektur. Die letzten Schritte wären Controller, die die Ereigniserzeugung an einem HTTP-Endpunkt Ihrer Anwendung handhaben, und dann ein Call Status Projector. Dieser Projektor, ähnlich dem vorherigen Beispiel, wäre in der Lage, einen Strom von Ereignissen zu verarbeiten, einen optionalen Zeitstempel zu erfassen und dann den korrekten Status der CallID zu dem von Ihnen angegebenen Zeitstempel (oder standardmäßig zum neuesten) zu berechnen.

Was sind die Vorteile von Event Sourcing?

  • Ereignisgesteuerte Architektur kann umstrukturiert werden. Da das System von den eingehenden Zustandsdaten entkoppelt ist, können Sie Ihre neuen Controller mit neuer Logik ausführen (z. B. um einen neuen Ereignistyp zu verarbeiten) und Ihre Datenströme für die Projektion neu aufbauen.

  • Vollständige Audit-Pfade unabhängig von der Zeit: Dies ist sehr wichtig für stark regulierte Branchen, wie wir in Bereichen wie dem Finanz- und Gesundheitswesen gesehen haben.

  • Trennung von Reads vs. Writes: Mit dem oben erwähnten CQRS wird die Art und Weise, wie auf Daten zugegriffen wird und wie sie geschrieben werden, aufgeteilt. Wenn Sie eine Anwendung abstimmen, die einen hohen Input/Output an Daten erfordert, können Sie einzelne Aspekte Ihrer Architektur entsprechend skalieren.

Was sind die Nachteile von Event Sourcing?

Ich kann mich nicht als vernünftiger Ingenieur bezeichnen, ohne auf die Nachteile hinzuweisen, und die sind ziemlich groß

  • Rückwärtskompatibilität: Ich habe erwähnt, wie schnell es sein kann, neue Änderungen in Ihre Modelle einzupflegen und Ihre Event-Handler neu zu starten, aber nicht, wann etwas veraltet ist. Es muss über die Ereignisbehandlung, die semantische Versionierung und das Verhalten nachgedacht werden, was dazu führen kann, dass große Mengen alter Daten gespeichert werden müssen, während komplexe ETL-Verfahren durchgeführt werden müssen.

  • Systemkomplexität: Dies ist die größte zu überwindende Hürde. Ich habe an ereignisgesteuerten Systemen gearbeitet, die in der Vergangenheit hochskaliert wurden, und der größte Teil dieser Komplexität ist ähnlich wie bei Microservices. Wenn Sie Ihr Ereignissystem nicht von Anfang an richtig architektiert haben nicht von Anfang an richtig geplant, dann kann ich Ihnen versichern, dass eine sich ändernde Enum nur darauf wartet, Ihr ganzes System zum Stolpern zu bringen

  • Fehlersuche: Eine der größten Frustrationen bei der Softwareentwicklung ist meiner Meinung nach das Fehlen der richtigen Debugging-Tools. Ich habe oft gesprochen über Ich habe oft über meine Bestürzung darüber gesprochen, dass Derick Rethans xdebug nicht Teil eines PHP-Stacks ist; ich verstehe einfach nicht, wie jemand ohne es programmieren kann, anstatt die Laufzeit zu beenden und einzelne Arrays oder Strings auszulesen. Ähnlich verhält es sich mit dem Debuggen von Microservices, was mich zu Event-Sourcing-Umgebungen führt: Um Streams genau debuggen zu können, müssen Sie wahrscheinlich zusätzliche benutzerdefinierte Tools für Ihr System schreiben, wenn etwas schief läuft

  • Speicherung: Dies ist vielleicht ein wenig offensichtlich, aber anstatt eine einzige Quelle der Wahrheit zu haben (in unserem Beispiel war es die Speicherung der Vonage Voice API Webhooks), haben Sie jetzt mehrere zusätzliche Tabellen, die eine Menge zusätzliche Daten, die zusätzlich zu den Quelldaten erstellt werden

Machen Sie es auf Ihre Art

Nach dem, was wir gesehen haben, ist die Vanille-Struktur dafür ziemlich komplex. Die gute Nachricht ist, dass es Bibliotheken gibt, die uns bei der Implementierung helfen. Hier sind meine Empfehlungen für jede Backend-Sprache:

Schlussfolgerung

Wie bei jedem neuen technischen Muster, das auftaucht, bin ich oft zynisch darüber, wie nützlich diese Dinge in der Praxis sind. Ich habe Gespräche über hexagonale Architekturen und React-Frontend-Applikationen mit umgekehrter Vererbung gesehen, und ich kann mir einfach nicht vorstellen, warum Entwickler wollen dieses Maß an Komplexität wollen. Im Fall von Event Sourcing ist die Situation meiner Meinung nach jedoch ganz anders. Ja, es ist schwierig zu implementieren und zu entwerfen, aber wie bei allem, was ist Ihr Anwendungsfall? Wenn Sie in einem vertikalen Markt tätig sind, der ein hohes Maß an Datenkontrolle erfordert, dann wird die Einführung von Event Sourcing Ihr Unternehmen von Anfang an vor potenziell großen Kopfschmerzen bewahren.

Haben Sie eine Frage oder möchten Sie uns mitteilen, was Sie gerade bauen?

Bleiben Sie auf dem Laufenden und halten Sie sich über die neuesten Nachrichten, Tipps und Veranstaltungen für Entwickler auf dem Laufenden.

Teilen Sie:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeSenior PHP Entwickler Advocate

Als ausgebildeter Schauspieler mit einer Dissertation in Standup-Comedy bin ich über die Meetup-Szene zur PHP-Entwicklung gekommen. Man findet mich, wenn ich über Technik spreche oder schreibe, oder wenn ich seltsame Platten aus meiner Vinylsammlung spiele oder kaufe.