Event-Driven Architecture mit Apache Kafka

Event-Driven Architecture mit Apache Kafka

Event-Driven Architecture mit Apache Kafka

Artikel-Übersicht

Apache Kafka ist nach Franz Kafka benannt und hat mit Literatur nichts zu tun — der Name geht auf das Entwicklerteam bei LinkedIn zurück, das die Plattform ursprünglich gebaut hat. Diese Klarstellung gleich zu Beginn, weil Google den Schriftsteller regelmäßig in dieselben Suchergebnisse mischt. Der Artikel trennt danach sauber zwischen dem Muster — ereignisgetriebene Architektur — und der Plattform Kafka, weil beides ständig verwechselt wird. Der zentrale Abschnitt erklärt den Unterschied, aus dem fast alles Weitere folgt: Kafka ist ein persistentes Log, keine Warteschlange. Es folgen ein lauffähiges Beispiel, die gängigen Ereignismuster mit ihren Kosten, ein fairer Vergleich mit RabbitMQ und fünf Fallstricke aus der Praxis. Zum Schluss die ehrliche Frage, wann sich das Muster nicht lohnt.

Was ereignisgetriebene Architektur ist

Bei ereignisgetriebener Architektur teilen Dienste mit, dass etwas geschehen ist, statt einander aufzufordern, etwas zu tun.

Der Unterschied klingt subtil und ändert alles:

Befehl Ereignis
Formulierung „Erstelle eine Rechnung" „Ein Auftrag wurde abgeschlossen"
Zeitform Imperativ Vergangenheit
Empfänger genau einer, bekannt beliebig viele, unbekannt
Absender weiß wer es tun soll nur, dass es passiert ist
Neuer Empfänger Absender muss geändert werden Absender bleibt unverändert

Die letzte Zeile ist der eigentliche Gewinn. Wenn die Buchhaltung künftig auch über abgeschlossene Aufträge informiert werden soll, ändert sich beim Vertrieb nichts — sie hört einfach mit. Genau das meint Entkopplung.

Der Preis: Der Absender weiß nicht mehr, ob etwas passiert ist. Bei einem Befehl bekommt man eine Antwort. Bei einem Ereignis muss man die Verarbeitung überwachen, sonst merkt niemand, dass ein Empfänger seit drei Tagen ausgefallen ist.

Was Apache Kafka ist

Kafka ist ein verteiltes, persistentes Log — Ereignisse werden angehängt und bleiben liegen, statt beim Lesen zu verschwinden.

Dieser eine Satz ist der Schlüssel zum ganzen System. Wer Kafka als Warteschlange denkt, wundert sich über fast jedes Verhalten.

Wie Kafka funktioniert

Vier Begriffe genügen für den Anfang:

Topic. Ein benannter Strom von Ereignissen, etwa auftrag.abgeschlossen. Vergleichbar mit einer Tabelle, in die nur angehängt wird.

Partition. Jedes Topic ist in Partitionen aufgeteilt. Innerhalb einer Partition ist die Reihenfolge garantiert, über Partitionen hinweg nicht. Welche Partition ein Ereignis bekommt, entscheidet sein Schlüssel — dieselbe Kundennummer landet immer in derselben Partition und behält damit ihre Reihenfolge.

Offset. Die Position eines Ereignisses in seiner Partition. Der Konsument merkt sich seine Position, nicht der Broker. Daraus folgt die wichtigste Eigenschaft: Man kann zurückspulen und alles noch einmal lesen.

Konsumentengruppe. Mehrere Instanzen desselben Dienstes teilen sich die Partitionen eines Topics. Verschiedene Gruppen lesen unabhängig voneinander — jede hat ihre eigene Position.

Aus diesen vier Begriffen folgt der Rest fast von selbst. Ein neuer Dienst, der auf ein bestehendes Topic hört, bekommt eine eigene Gruppe und kann die Historie von vorn lesen, ohne dass die anderen etwas merken.

Kafka ist keine Warteschlange

Warteschlange (z. B. RabbitMQ) Log (Kafka)
Nach dem Lesen Nachricht ist weg Ereignis bleibt liegen
Aufbewahrung bis zur Verarbeitung konfigurierte Dauer oder Größe
Mehrere Empfänger über zusätzliche Verteilung eingebaut, unabhängige Gruppen
Wiederholung nur bei Fehler, aus Warteschlange jederzeit, durch Zurücksetzen der Position
Reihenfolge je Warteschlange je Partition
Position merkt sich der Broker der Konsument
Typischer Einsatz Arbeitsaufträge verteilen Ereignisströme, mehrere Interessenten

Die Zeile „Wiederholung" ist der praktische Hauptgewinn: Wenn ein Verarbeitungsfehler eine Woche lang falsche Ergebnisse erzeugt hat, korrigiert man den Code und liest die Woche erneut. Mit einer Warteschlange geht das nicht — die Nachrichten sind weg.

Ein erstes Beispiel

Eine lokale Umgebung mit einem Broker:

services:
  kafka:
    image: apache/kafka:latest
    ports:
      - "9092:9092"
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

Bemerkenswert daran ist, was fehlt: ZooKeeper. Kafka verwaltet seine Metadaten inzwischen selbst; ältere Anleitungen im Netz zeigen noch den Aufbau mit einem separaten Koordinationsdienst.

Ein Producer und ein Consumer in Python:

from kafka import KafkaProducer, KafkaConsumer
import json

producer = KafkaProducer(
    bootstrap_servers="localhost:9092",
    key_serializer=lambda k: k.encode(),
    value_serializer=lambda v: json.dumps(v).encode(),
)

producer.send(
    "auftrag.abgeschlossen",
    key="kunde-4711",                    # bestimmt die Partition
    value={"auftragId": "A-2026-0042", "betrag": 149.90},
)
producer.flush()
consumer = KafkaConsumer(
    "auftrag.abgeschlossen",
    bootstrap_servers="localhost:9092",
    group_id="rechnungsstellung",        # eigene Gruppe, eigene Position
    auto_offset_reset="earliest",        # beim ersten Start von vorn lesen
    value_deserializer=lambda v: json.loads(v.decode()),
)

for nachricht in consumer:
    print(nachricht.partition, nachricht.offset, nachricht.value)

Der Schlüssel kunde-4711 ist kein Beiwerk: Er sorgt dafür, dass alle Ereignisse dieses Kunden in derselben Partition landen und damit in Reihenfolge verarbeitet werden. Wer keinen Schlüssel setzt, bekommt Gleichverteilung — und verliert jede Reihenfolgegarantie.

Die Ereignismuster

Muster Problem Wann sinnvoll Kosten
Ereignisbenachrichtigung „etwas ist passiert", Empfänger holt Details selbst Standardfall, geringste Kopplung Rückfrage nötig
Event-Carried State Transfer Ereignis enthält alle nötigen Daten wenn Rückfragen zu teuer sind größere Ereignisse, Datenduplikate
Outbox Datenänderung und Veröffentlichung atomar sobald Ereignisse aus einer Transaktion kommen Tabelle plus Relay
Saga fachliche Transaktion über Dienstgrenzen wenn unvermeidbar hoch, Kompensationslogik
CQRS Lese- und Schreibmodell trennen bei stark unterschiedlicher Last zwei Modelle, Synchronisation
Event Sourcing Zustand als Folge von Ereignissen speichern wenn die Historie fachlich gefordert ist sehr hoch — eigenes Vorhaben

Zur letzten Zeile ein deutlicher Hinweis: Event Sourcing ist kein Nebenprodukt von Kafka. Es verlangt Antworten auf Schemaänderungen alter Ereignisse, auf Auswertungen über die Historie, auf Datenschutzlöschungen und auf die Frage, wie man einen Fehler korrigiert, der vor zwei Jahren passiert ist. Wer es einführt, weil es zum Thema zu gehören scheint, hat sich ein zweites Projekt gekauft. Dass auch Domain-Driven Design es nicht voraussetzt, ist dort ausführlich begründet.

Saga und Outbox behandle ich aus der Dienstperspektive in Microservices bauen.

Ereignisse schneiden und benennen

Drei Regeln, die sich bewährt haben:

  • Vergangenheitsform. AuftragAbgeschlossen, nicht AuftragAbschliessen. Der Name entscheidet mit darüber, ob jemand das Ereignis als Befehl missversteht.
  • Fachliche Namen, keine technischen. RechnungStorniert sagt etwas, RechnungUpdated nicht.
  • Schemata versionieren. Ein Ereignis ist eine Schnittstelle mit allen Folgen — dieselben Fragen wie in API-Design, nur dass die Empfänger noch weniger bekannt sind.

Kafka oder RabbitMQ?

Kafka RabbitMQ
Modell persistentes Log Warteschlange mit Vermittlung
Stärke Durchsatz, Wiederholbarkeit, viele Interessenten flexible Verteilung, Prioritäten, Verzögerung
Betrieb aufwendiger, verteiltes System leichter, ein Dienst genügt oft
Reihenfolge je Partition je Warteschlange
Gut für Ereignisströme, Auswertungen, Wiederaufbereitung Arbeitsaufträge, Aufgabenverteilung

Die ehrliche Empfehlung: Für Aufgabenverteilung ist RabbitMQ einfacher und völlig ausreichend. Für Ereignisströme mit mehreren unabhängigen Konsumenten und der Möglichkeit, Vergangenes erneut zu lesen, ist Kafka richtig.

Wer Kafka einführt, weil es der bekanntere Name ist, handelt sich einen erheblichen Betriebsaufwand ein — und merkt es meist erst beim ersten Vorfall.

Die fünf Fallstricke

1. Reihenfolge gilt nur je Partition. Symptom: Ereignisse desselben Kunden werden in falscher Reihenfolge verarbeitet. Ursache: kein oder falscher Schlüssel. Gegenmaßnahme: Als Schlüssel die fachliche Entität wählen, deren Reihenfolge zählt.

2. Genau-einmal-Zustellung ist teuer und selten nötig. Symptom: jemand baut komplizierte Transaktionslogik, um Duplikate zu vermeiden. Gegenmaßnahme: Konsumenten idempotent bauen. Das ist einfacher, robuster und funktioniert auch, wenn die Zusage einmal nicht greift.

3. Schemaänderungen brechen Konsumenten still. Symptom: Ein Feld wurde umbenannt, ein Konsument verarbeitet seitdem falsche Daten, ohne zu scheitern. Gegenmaßnahme: Eine Schemaverwaltung mit Kompatibilitätsprüfung, oder mindestens ein verbindlicher Prozess für Ereignisänderungen.

4. Die Aufbewahrungsdauer ist eine fachliche Entscheidung. Symptom: Ein neuer Dienst soll die Historie lesen — sie ist seit sieben Tagen gelöscht. Gegenmaßnahme: Vor dem ersten Topic klären, wie lange Ereignisse verfügbar bleiben müssen, und das dokumentieren.

5. Kafka ist ein verteiltes System mit eigenem Betriebsaufwand. Symptom: Der Cluster braucht Aufmerksamkeit, die niemand eingeplant hat. Gegenmaßnahme: Entweder Betriebskompetenz aufbauen oder ein verwaltetes Angebot nutzen — für den DACH-Raum gibt es Angebote mit Rechenzentren in der EU und der Schweiz, was für viele Projekte das Auswahlkriterium ist.

Wann das Muster nicht lohnt

  • Wenige Dienste. Bei drei Diensten ist ein synchroner Aufruf nachvollziehbarer als ein Ereignis, das irgendwo verarbeitet wird.
  • Keine Betriebskompetenz. Ein Ereignisstrom, den niemand überwacht, ist schlechter als ein Aufruf, der sichtbar fehlschlägt.
  • Wenn eine Antwort gebraucht wird. Ereignisse sind einseitig. Wer auf ein Ergebnis warten muss, braucht einen synchronen Aufruf.

Ob die Verteilung überhaupt die richtige Entscheidung ist, behandelt Microservices oder Monolith.

Und ein Anwendungsfall aus dem KI-Umfeld, weil er das Muster gut zeigt: Wenn sich Dokumente ändern, muss ein Suchindex nachgezogen werden. Ein Ereignisstrom dokument.geaendert, auf den ein Indexierungsdienst hört, ist dafür die natürliche Form — der Dienst, der das Dokument ändert, muss nichts über die Indexierung wissen. Wie so eine Indexierung aussieht, steht in RAG selbst bauen.

Häufige Fragen

Was ist Apache Kafka? Ein verteiltes, persistentes Log für Ereignisströme. Ereignisse werden angehängt und bleiben für eine konfigurierte Dauer liegen, statt beim Lesen zu verschwinden.

Warum heißt Apache Kafka so? Der Name wurde vom Entwicklerteam bei LinkedIn gewählt, weil das System für das Schreiben optimiert ist — eine Anspielung auf den Schriftsteller, ohne inhaltlichen Bezug.

Wie funktioniert Apache Kafka? Über Topics, die in Partitionen aufgeteilt sind. Der Schlüssel eines Ereignisses bestimmt die Partition, die Position merkt sich der Konsument. Daraus folgen Reihenfolgegarantie je Partition und die Möglichkeit, erneut zu lesen.

Was ist ereignisgetriebene Architektur? Ein Entwurfsansatz, bei dem Dienste mitteilen, dass etwas geschehen ist, statt einander aufzufordern, etwas zu tun. Das entkoppelt Absender und Empfänger.

Kafka oder RabbitMQ? RabbitMQ für Aufgabenverteilung, Kafka für Ereignisströme mit mehreren unabhängigen Konsumenten und der Möglichkeit, Vergangenes erneut zu verarbeiten.

Braucht Kafka noch ZooKeeper? Nein. Kafka verwaltet seine Metadaten inzwischen selbst; ältere Anleitungen zeigen noch den Aufbau mit separatem Koordinationsdienst.

Zusammenfassung und praktische Tipps

Ereignisgetriebene Architektur entkoppelt und verschiebt die Komplexität in die Fehlerbehandlung. Die wichtigsten Punkte:

  • Kafka ist ein Log, keine Warteschlange. Aus diesem einen Unterschied folgt fast alles.
  • Ereignisse sind Vergangenheit, Befehle sind Imperativ. Der Name entscheidet mit über die Kopplung.
  • Der Schlüssel bestimmt die Partition — und damit, ob die Reihenfolge stimmt.
  • Konsumenten idempotent bauen ist einfacher als jede Zustellgarantie.
  • Event Sourcing ist ein eigenes Vorhaben, kein Nebenprodukt.
  • Für Aufgabenverteilung ist RabbitMQ die einfachere Wahl.
  • Die Aufbewahrungsdauer ist eine fachliche Entscheidung und sollte vor dem ersten Topic feststehen.

Mein Rat für den Einstieg: Bau das Beispiel oben lokal auf und setz einmal bewusst die Konsumentenposition zurück, um denselben Strom noch einmal zu lesen. Dieser eine Handgriff macht den Unterschied zwischen Log und Warteschlange greifbarer als jede Erklärung — und er zeigt dir sofort, ob dein Konsument idempotent ist.

Ressourcen