Bevor wir uns ins Getümmel stürzen, erinnern wir uns daran, warum wir uns überhaupt in Event Sourcing verliebt haben:

  • Vollständige Audit-Trail? Check.
  • Fähigkeit, vergangene Zustände zu rekonstruieren? Check.
  • Flexibilität, unser Domänenmodell weiterzuentwickeln? Check.
  • Skalierbarkeit und Leistungsverbesserungen? Doppel-Check.

Es klang alles zu gut, um wahr zu sein. Spoiler-Alarm: Das war es auch.

Das Setup: Unser Bestandsverwaltungssystem

Unser System wurde entwickelt, um Millionen von SKUs über mehrere Lager hinweg zu verwalten. Wir wählten Event Sourcing, um eine präzise Historie jeder Bestandsbewegung, Preisänderung und Aktualisierung von Artikelattributen zu führen. Der Event Store war unsere Quelle der Wahrheit, mit Projektionen, die den aktuellen Zustand für schnelle Abfragen bereitstellten.

Hier ist eine vereinfachte Version unserer Event-Struktur:

{
  "eventId": "e123456-7890-abcd-ef12-34567890abcd",
  "eventType": "StockAdded",
  "aggregateId": "SKU123456",
  "timestamp": "2023-04-01T12:00:00Z",
  "data": {
    "quantity": 100,
    "warehouseId": "WH001"
  },
  "version": 1
}

Sieht harmlos genug aus, oder? Oh, wie naiv wir waren.

Die Entwirrung: Fallstricke der Event-Versionierung

Unser erster großer Stolperstein kam, als wir unser StockAdded-Event aktualisieren mussten, um ein reason-Feld hinzuzufügen. Einfach genug, dachten wir. Wir erhöhen einfach die Version und fügen eine Migrationsstrategie hinzu. Was könnte schiefgehen?

Alles. Alles könnte schiefgehen.

Lektion 1: Versioniere deine Events, als hinge dein Leben davon ab

Wir machten den klassischen Fehler, eine einzige Versionsnummer für alle Events zu verwenden. Das bedeutete, dass wir, als wir StockAdded aktualisierten, unbeabsichtigt die Verarbeitung aller anderen Events brachen.

Hier ist, was wir hätten tun sollen:

{
  "eventType": "StockAdded",
  "eventVersion": 2,
  "data": {
    "quantity": 100,
    "warehouseId": "WH001",
    "reason": "Initial stock"
  }
}

Indem wir jeden Event-Typ unabhängig versionierten, hätten wir den Dominoeffekt vermeiden können, der unser System in die Knie zwang.

Lektion 2: Migrationen sind nicht optional

Wir dachten zunächst, wir könnten einfach beide Versionen in unseren Event-Handlern behandeln. Großer Fehler. Als das System wuchs, wurde dieser Ansatz unhaltbar.

Stattdessen hätten wir eine robuste Migrationsstrategie implementieren sollen:


def migrate_stock_added_v1_to_v2(event):
    if event['eventVersion'] == 1:
        event['data']['reason'] = 'Legacy import'
        event['eventVersion'] = 2
    return event

# Migrationen beim Lesen aus dem Event Store anwenden
events = [migrate_stock_added_v1_to_v2(e) for e in read_events()]

Die Snapshot-Saga: Wenn Optimierungen nach hinten losgehen

Als unser Event Store wuchs, wurde der Wiederaufbau von Projektionen schmerzhaft langsam. Hier kamen Snapshots ins Spiel: unser vermeintlicher Retter, der sich in einen weiteren Albtraum verwandelte.

Lektion 3: Die Snapshot-Frequenz ist ein empfindliches Gleichgewicht

Wir erstellten zunächst alle 100 Events Snapshots. Das funktionierte gut, bis wir einen plötzlichen Anstieg der Transaktionen erlebten, was dazu führte, dass unsere Snapshot-Erstellung ins Hintertreffen geriet und unsere Projektionen zunehmend veraltet wurden.

Die Lösung? Adaptive Snapshot-Frequenz:


def should_create_snapshot(aggregate):
    time_since_last_snapshot = current_time() - aggregate.last_snapshot_time
    events_since_last_snapshot = aggregate.event_count - aggregate.last_snapshot_event_count
    
    return (time_since_last_snapshot > MAX_TIME_BETWEEN_SNAPSHOTS or
            events_since_last_snapshot > MAX_EVENTS_BETWEEN_SNAPSHOTS)

Lektion 4: Auch Snapshots brauchen Versionierung

Wir vergaßen, unsere Snapshots zu versionieren. Als wir unsere Aggregatstruktur änderten, brach das Chaos aus. Ältere Snapshots wurden inkompatibel, und wir konnten unsere Projektionen nicht wiederherstellen.

Die Lösung? Versioniere deine Snapshots und biete Upgrade-Pfade an:


def upgrade_snapshot(snapshot):
    if snapshot['version'] == 1:
        snapshot['data']['newField'] = calculate_new_field(snapshot['data'])
        snapshot['version'] = 2
    return snapshot

# Beim Laden von Snapshots verwenden
snapshot = upgrade_snapshot(load_snapshot(aggregate_id))

Das Korruptionsdilemma: Wenn deine Quelle der Wahrheit lügt

Der letzte Nagel in unserem Sarg war die Korruption des Event Stores. Ein perfekter Sturm aus Netzwerkproblemen, einem Fehler in unserem Event Store und einer übermäßig aggressiven Fehlerbehandlung führte zu doppelten und fehlenden Events.

Lektion 5: Vertrauen, aber überprüfen

Wir vertrauten blind unserem Event Store. Stattdessen hätten wir Prüfsummen und regelmäßige Integritätsprüfungen implementieren sollen:


def verify_event_integrity(event):
    expected_hash = calculate_hash(event['data'])
    return event['hash'] == expected_hash

def perform_integrity_check():
    for event in read_all_events():
        if not verify_event_integrity(event):
            raise IntegrityError(f"Corrupt event detected: {event['eventId']}")

Lektion 6: Implementiere eine Wiederherstellungsstrategie

Wenn Korruption passiert (und das wird sie), brauchst du einen Weg zur Wiederherstellung. Wir hatten keinen, und das kostete uns teuer. Hier ist, was wir hätten tun sollen:

  1. Führe ein separates, nur anhängbares Protokoll aller eingehenden Befehle.
  2. Implementiere einen Abgleichsprozess, um das Befehlsprotokoll mit dem Event Store zu vergleichen.
  3. Erstelle einen Wiederherstellungsprozess, um fehlende Events erneut abzuspielen oder Duplikate zu entfernen.

def reconcile_events():
    command_log = read_command_log()
    event_store = read_event_store()
    
    for command in command_log:
        if not event_exists_for_command(command, event_store):
            replay_command(command)
    
    for event in event_store:
        if is_duplicate_event(event, event_store):
            remove_duplicate_event(event)

Der Phönix erhebt sich: Wiederaufbau mit Resilienz

Nach unzähligen schlaflosen Nächten und mehr Kaffee, als ich zugeben möchte, stabilisierten wir schließlich unser System. Hier sind die wichtigsten Erkenntnisse, die uns halfen, aus der Asche aufzusteigen:

  • Event-Versionierung ist nicht optional – mach es von Anfang an.
  • Implementiere robuste Migrationsstrategien für sowohl Events als auch Snapshots.
  • Adaptive Snapshot-Erstellung balanciert Leistung und Konsistenz.
  • Vertraue nichts – implementiere Integritätsprüfungen auf jeder Ebene.
  • Habe eine klare Wiederherstellungsstrategie, bevor du sie brauchst.
  • Umfassende Tests, einschließlich Chaos-Engineering, können dir den Tag retten.

Fazit: Das zweischneidige Schwert des Event Sourcing

Event Sourcing ist mächtig, aber auch komplex. Es ist kein Allheilmittel und erfordert sorgfältige Überlegungen und robuste Ingenieurpraktiken, um in der Produktion erfolgreich zu sein.

Denke daran, mit großer Macht kommt große Verantwortung – und im Fall von Event Sourcing viele schlaflose Nächte. Aber mit diesen Lektionen bist du jetzt besser vorbereitet, die Herausforderungen des Event Sourcing in freier Wildbahn zu meistern.

Und jetzt, wenn du mich entschuldigen würdest, habe ich ein wenig PTSD zu verarbeiten. Kennt jemand einen guten Therapeuten, der sich auf Event Sourcing-Trauma spezialisiert hat?

"Beim Event Sourcing, wie im Leben, geht es nicht darum, Fehler zu vermeiden – es geht darum, anmutig zu scheitern und stärker zurückzukommen."

Weiterführende Lektüre

Hast du deine eigenen Event Sourcing-Dämonen bekämpft? Teile deine Kriegsgeschichten in den Kommentaren – Elend liebt Gesellschaft, schließlich!