Event-driven klingt fast immer nach der moderneren Architektur. Ein Service veröffentlicht ein Event, andere reagieren darauf, niemand kennt den anderen, alles ist entkoppelt. Im Diagramm funktioniert das hervorragend.
Im Produktionssystem kauft man sich mit dieser Entkopplung aber etwas ein: verzögerte Konsistenz, Wiederholungen, Reihenfolgeprobleme, schwerere Fehlersuche und Geschäftsprozesse, deren Zustand plötzlich über mehrere Systeme verteilt ist.
Deshalb ist meine Voreinstellung nicht event-driven. Wenn ein Service jetzt eine Antwort von einem anderen braucht, rufe ich ihn synchron auf. Events kommen ins Spiel, wenn der Produzent seine Arbeit bereits erledigt hat und andere Teile des Systems unabhängig darauf reagieren sollen. Das klingt nach einem kleinen Unterschied. Für die Architektur ist es ein ziemlich großer.
Synchron heißt nicht schlecht gekoppelt
Nehmen wir einen Checkout. Der Kunde klickt auf „Bestellung abschicken". Bevor ich ihm sage, dass die Bestellung angenommen ist, will ich wissen: Gibt es den Artikel? Ist der Preis noch gültig? Lässt sich die Zahlung autorisieren? Darf dieser Kunde bestellen?
Wenn diese Antworten Teil der Entscheidung sind, ob die Bestellung überhaupt entstehen darf, sehe ich wenig Grund, daraus künstlich einen asynchronen Prozess zu machen:
Checkout
|
+--> Pricing Service
|
+--> Payment Service
|
+--> Order Service
|
v
Antwort an den KundenREST oder gRPC sind dafür völlig legitime Werkzeuge. Der Aufrufer wartet auf eine Antwort. Wird die Zahlung abgelehnt, erfährt er es sofort. Ist der Payment-Service nicht erreichbar, kann der Checkout darauf reagieren. Der Kontrollfluss ist sichtbar.
Diese Abhängigkeit ist kein Architekturfehler, sie bildet die fachliche Realität ab. Wenn Prozess A ohne das Ergebnis von Prozess B keine Entscheidung treffen kann, sind A und B fachlich gekoppelt. Ein Event Bus lässt diese Kopplung nicht verschwinden. Er macht sie nur schwerer zu sehen.
Welches Protokoll für den direkten Aufruf passt, steht in REST oder gRPC für Backend-Services?
Entkopplung, die keine ist
„Wir müssen die Services entkoppeln" höre ich bei Microservices häufig. Dann wird aus einem direkten Aufruf vom Order Service zum Payment Service etwas vermeintlich Eleganteres:
Order Service
|
OrderRequested
|
v
Event Bus
|
v
Payment Service
|
PaymentProcessed
|
v
Event Bus
|
v
Order ServiceAuf Netzwerkebene kennen sich die beiden Services jetzt nicht mehr. Fachlich kennt der Order Service den Payment-Prozess aber so genau wie vorher, denn er kann die Bestellung nicht abschließen, bevor er weiß, ob die Zahlung durchging. Die Kopplung ist noch da. Dazugekommen sind zwei Events, ein Zwischenzustand, Event-Schemas, Wiederholungs- und Timeout-Logik, die Zuordnung zwischen Anfrage und Antwort, der Umgang mit verlorenen oder verspäteten Antworten, eine Dead-Letter Queue und Observability über mehrere asynchrone Schritte.
Ich habe nicht entkoppelt. Ich habe aus einem Funktionsaufruf ein verteiltes Protokoll gebaut. Das kann nötig sein. Aber ich will dafür einen besseren Grund als „event-driven skaliert besser".
Frage oder Tatsache
Mein erster Filter ist deshalb eine einzige Frage: Braucht der Aufrufer das Ergebnis jetzt? Kann Service A ohne die Antwort von Service B nicht sinnvoll weitermachen, beginne ich synchron.
Kann dieser Kunde bezahlen?
Wie hoch ist der aktuelle Preis?
Existiert dieser Datensatz?
Ist dieses Konto gesperrt?
Darf dieser Benutzer diese Aktion ausführen?Das sind Fragen, und eine Frage erwartet eine Antwort. Anders sieht es aus, wenn etwas bereits passiert ist:
OrderCreated
PaymentCaptured
CustomerRegistered
InvoiceIssued
SubscriptionCancelledDas sind keine Fragen, das sind Tatsachen, und Tatsachen eignen sich hervorragend für Events. Ich mache den Unterschied auch in den Namen sichtbar: CreateInvoice, SendWelcomeEmail oder UpdateCRM sind Befehle. Ein Event steht in der Vergangenheitsform, weil es etwas beschreibt, das schon geschehen ist.
Das hat eine wichtige Folge: Der Produzent muss nicht wissen, wer darauf reagiert. Sobald eine Bestellung verbindlich angelegt ist, veröffentlicht der Order Service OrderCreated. Darauf reagieren vielleicht E-Mail, Analytics, CRM, Fulfillment, Data Warehouse und ein Loyalty-Service. Kommt morgen eine Betrugserkennung dazu, ändere ich den Order Service nicht, der neue Consumer hört einfach mit.
Hier entsteht echte Entkopplung. Nicht weil kein HTTP mehr verwendet wird, sondern weil der Produzent fachlich nichts über die Reaktion wissen muss. Das ist für mich der sauberste Anwendungsfall für Event-driven Architecture überhaupt.
Ein Bestellprozess enthält beides
Regeln wie „bei uns kommunizieren Services nur über Events" halte ich für eine Einschränkung, die man sich ohne Not auferlegt. Ein realistischer Ablauf sieht bei mir so aus:
synchron
Browser ----------------------------> Order API
|
| synchron
v
Payment Service
|
v
Bestellung speichern
|
OrderCreated
|
+---------------+---------------+
| | |
v v v
E-Mail Analytics FulfillmentDer kritische Teil bleibt synchron, der Kunde bekommt sofort eine Antwort. Alles, was danach passieren kann, ohne dass diese Antwort davon abhängt, läuft asynchron. Die interessantere Frage ist deshalb nicht „synchron oder event-driven?", sondern: Wo endet der Vorgang, der jetzt eine Antwort braucht? Alles dahinter ist ein Kandidat für Asynchronität.
Das einfachste Beispiel ist die Willkommensmail. Muss die Antwort auf POST /customers warten, bis der Mailserver die Nachricht angenommen hat? Natürlich nicht. Ich speichere den Kunden, die Registrierung ist abgeschlossen, danach entsteht CustomerRegistered. Ist der Mailanbieter drei Minuten lang weg, bleibt der Kunde trotzdem registriert, und die Nachricht wird später zugestellt. Registrierung und Mailversand haben unterschiedliche Verfügbarkeitsanforderungen, also dürfen sie unterschiedliche Prozesse sein.
Queue und Event Bus sind nicht dasselbe
An dieser Stelle wird vieles unnötig unter „event-driven" zusammengefasst. „Bitte verarbeite dieses Bild" ist nicht dasselbe wie „Dieses Bild wurde verarbeitet". Das Erste ist Arbeit, das Zweite eine Tatsache.
Für Arbeit nehme ich eine Queue. Der Upload Service will, dass etwas erledigt wird, und genau ein Worker soll es übernehmen:
Upload Service --> SQS Queue --> Image WorkerFür eine fachliche Tatsache passt Publish/Subscribe besser, weil sich mehrere Systeme unabhängig für dasselbe Ereignis interessieren:
OrderCreated
|
EventBridge
|
+----+------+
| | |
v v v
CRM Mail AnalyticsQueue: Jemand soll Arbeit erledigen. Event: Etwas ist passiert, und wer sich dafür interessiert, darf reagieren. Beides ist asynchron, aber die Bedeutung ist eine andere.
Zwei Fälle, in denen ich schnell Richtung Asynchronität gehe, folgen genau aus dieser Trennung. Der erste sind Lastspitzen: Kommen innerhalb von drei Minuten 500.000 Datensätze an, deren Verarbeitung sich über eine Stunde verteilen darf, müsste ich synchron beide Seiten auf denselben Peak auslegen. Mit einer Queue nimmt der Produzent schnell an, die Queue wächst, und ein Worker-Pool arbeitet sie nach eigener Kapazität ab. Das bringt nicht Architekturästhetik, sondern Belastbarkeit. Der zweite ist Fan-out: Ruft der Order Service CRM, Mail, Analytics, Data Warehouse und Loyalty selbst auf, kennt er fünf Systeme und muss für das sechste geändert werden. Mit einem Event beschreibt er nur noch seine eigene fachliche Wahrheit, und was die Organisation damit macht, ist nicht mehr seine Verantwortung.
Wie aus nächtlichen Läufen eine Queue mit Wiederholung wird, steht in Von Cronjobs zu Queues.
Der Preis: Zustellung, Konsistenz, Lesbarkeit
Event-driven verschiebt Fehler, es beseitigt sie nicht. Bei einem synchronen Aufruf ist der Fehler meist sofort sichtbar: Der Payment-Service antwortet mit 500, und damit kann ich etwas anfangen. Eine Nachricht dagegen kann verarbeitet werden, später, mehrfach oder erst nach einem Retry. SQS und EventBridge sichern zu, dass eine Nachricht mindestens einmal ankommt, nicht genau einmal. Stirbt ein Consumer nach der Arbeit, aber vor der Bestätigung, wird die Nachricht nach Ablauf der Sichtbarkeitsfrist erneut zugestellt:
Nachricht erhalten
|
Rechnung erstellen
|
X Prozess stirbt vor der Bestätigung
|
Nachricht erneut zugestellt
|
Rechnung noch einmal erstellen?Lautet die Antwort „ja", habe ich ein Problem. Idempotenz gehört bei asynchroner Verarbeitung deshalb zur Grundausstattung. Die robuste Frage ist nicht „kommt diese Nachricht bestimmt nur einmal?", sondern: Was passiert, wenn sie zweimal kommt?
Dazu kommt die Konsistenz. Der Kundenservice speichert Kunde 4711 und veröffentlicht CustomerCreated. Das CRM verarbeitet das vielleicht 50 Millisekunden später, vielleicht zwei Sekunden, vielleicht fünf Minuten, weil es gerade einen Fehler gibt. So lange existiert der Kunde im einen System und im anderen noch nicht. Technisch ist das normal, fachlich muss es akzeptabel sein. Bei Analytics fast immer, bei einem Suchindex oft, bei einer Willkommensmail natürlich. Bei einer Kreditprüfung, ohne deren Ergebnis ich keinen Kredit auszahlen darf, wahrscheinlich nicht. Eventual Consistency ist keine technische Eigenschaft, die ich auswähle. Sie ist eine fachliche Entscheidung.
Und schließlich die Lesbarkeit. Ein synchroner Ablauf ist angenehm langweilig: A → B → C → Antwort, und wenn etwas schiefgeht, folge ich dem Request. Ereignisgetrieben kann derselbe Geschäftsprozess über vier Events und fünf Services verteilt sein. Fragt dann der Support „Warum wurde Bestellung 92815 nicht versendet?", muss ich herausfinden, ob OrderCreated veröffentlicht wurde, ob der Consumer es bekommen hat, in welcher Version, ob es mehrfach kam, ob es in der Dead-Letter Queue liegt, welche Correlation-ID dazugehört und ob der Vorgang gescheitert oder nur noch nicht fertig ist. Das ist kein Argument gegen Events. Es ist ihr Preis.
Eine ernsthafte Event-Architektur baue ich deshalb nicht ohne Observability. Je Event will ich mindestens Event-ID, Typ, Zeitpunkt, Produzent, Correlation-ID, fachliche ID, Consumer, Verarbeitungsstatus, Anzahl der Versuche und den Fehler sehen. Und ich will nicht nur Queue-Tiefe und Fehlerrate, sondern den fachlichen Verlauf:
Order 92815
created: 14:02:11
paid: 14:02:12
event emitted: 14:02:12
fulfillment: 14:02:13
mail: 14:02:14
analytics: 14:02:18Sonst entsteht beim ersten größeren Incident ein seltsamer Zustand: Alle Systeme funktionieren, nur die Bestellung nicht, und niemand weiß, wo sie hängt.
Wie der Trace-Kontext über eine Queue mitwandert, steht in OpenTelemetry in PHP und Go einführen.
Events sind APIs
Eine REST-API behandelt fast jeder automatisch als Vertrag. Bei Events passiert das erstaunlich oft nicht. Einer erweitert das JSON, ein anderer benennt ein Feld um, ein dritter liest einen Status anders, und plötzlich erwarten drei von fünf Consumern noch die alte Struktur.
Ein veröffentlichtes Event ist für mich deshalb eine API, vielleicht sogar die gefährlichere. Bei einer synchronen API weiß ich meist, wer sie aufruft. Bei einem Event weiß der Produzent absichtlich nicht, wer alles zuhört. Also: Schema versionieren, Felder nicht leichtfertig entfernen, Bedeutung dokumentieren, kompatibel erweitern, Consumer unabhängig ausrollbar halten. Die Entkopplung trägt nur, wenn der Vertrag stabil ist. Sonst kennt der Produzent die Consumer zwar nicht, bricht sie aber mit jeder Änderung trotzdem.
Event-Ketten und der versteckte Workflow
Ein Event löst ein Event aus, das löst wieder eins aus, und nach ein paar Monaten sieht der Geschäftsprozess so aus:
A -> Event
|
B -> Event
|
C -> Event
|
DJeder Schritt sieht sauber entkoppelt aus. Der Prozess ist trotzdem fest gekoppelt, nur verteilt. Die Abhängigkeiten sind nicht verschwunden, sie sind unsichtbar geworden. Spätestens wenn die Reihenfolge fachlich zählt, frage ich: Habe ich hier unabhängige Reaktionen auf Ereignisse, oder eigentlich einen Workflow?
Nehmen wir Bestellung anlegen, Zahlung reservieren, Lager reservieren, Versand anstoßen. Jetzt scheitert die Lagerreservierung. Muss die Zahlung zurückgenommen werden? Was passiert, wenn das Zurücknehmen auch scheitert? Wer weiß, in welchem Zustand der Vorgang steckt? Das lässt sich vollständig über Events lösen, als Choreografie, in der jeder Service auf den vorigen reagiert. Das kann elegant sein, bis niemand mehr einen Ort findet, an dem der ganze Prozess sichtbar ist.
Meine Tendenz: Unabhängige Reaktionen dürfen choreografiert sein. Ein echter Geschäftsprozess braucht einen sichtbaren Besitzer. Das kann Anwendungscode sein, eine State Machine oder ein Workflow-System wie AWS Step Functions. Was genau, hängt vom Problem ab. Wichtiger ist, dass jemand den Zustand besitzt, auch den der Ausgleichsschritte, die das Saga-Muster beschreibt.
Wie so eine Kette im Betrieb aussieht, wenn niemand sie geplant hat, steht in Der verteilte Monolith.
Der gefährlichste Fehler: speichern und veröffentlichen
Ein klassischer Event-Ablauf sieht zunächst harmlos aus: Datensatz speichern, dann Event veröffentlichen. Stirbt der Prozess dazwischen, existiert die Bestellung, aber niemand erfährt davon. Andersherum ist es nicht besser: Das Event ist raus, die Datenbankänderung scheitert, und die Consumer reagieren auf etwas, das fachlich nie passiert ist. Das ist kein exotischer Randfall, sondern eine Systemgrenze, die ausdrücklich gelöst werden muss.
Das etablierte Muster dafür ist die Transactional Outbox. Fachlicher Zustand und Event landen in derselben lokalen Transaktion:
BEGIN;
INSERT INTO bestellungen (id, kunde_id, summe_cent, status)
VALUES ('92815', '4711', 12900, 'angelegt');
INSERT INTO outbox (id, typ, fachliche_id, nutzlast, angelegt_am)
VALUES ('evt-7f3a', 'OrderCreated', '92815', '{"summe_cent": 12900}', now());
COMMIT;Ein eigener Prozess liest die Outbox und veröffentlicht die Einträge, per Abfrage oder über Change Data Capture auf dem Datenbankprotokoll. Scheitert das Veröffentlichen, versucht er es erneut, und genau deshalb kommt das Event im Zweifel zweimal an, was der Consumer über seine Idempotenz abfängt, etwa mit der Event-ID als Schlüssel. Aus Datenbank und Message Broker wird so keine verteilte Transaktion, sondern zwei Schritte, von denen der zweite beliebig oft wiederholt werden darf.
Wo Events geschäftskritisch sind, halte ich diesen Teil für wichtiger als die Frage, welcher Event Bus es wird.
Dasselbe Grundproblem, zwei Schreibvorgänge ohne gemeinsame Transaktion, begegnet jeder Datenumstellung im Betrieb: Dual-Write und Backfill.
Meine Regeln, und wie ein Backend damit aussieht
Synchron, wenn der Aufrufer das Ergebnis jetzt braucht, die Operation Teil derselben fachlichen Entscheidung ist, ein Fehler direkt an den Aufrufer zurückgehen muss, der Ablauf überschaubar ist und Konsistenz wichtiger ist als zeitliche Entkopplung.
Eine Queue, wenn Arbeit zuverlässig später erledigt werden kann, Lastspitzen gepuffert werden sollen, Produzent und Worker unterschiedliche Kapazitäten haben, ein Auftrag von genau einem Consumer bearbeitet wird und Wiederholungen zum normalen Betrieb gehören.
Ein Event, wenn eine fachliche Tatsache bereits eingetreten ist, mehrere unabhängige Systeme darauf reagieren, der Produzent diese Consumer nicht kennen soll, neue Reaktionen ohne Änderung am Produzenten dazukommen sollen und Eventual Consistency fachlich erlaubt ist.
Das sind keine absoluten Regeln. Aber sie haben mir deutlich mehr geholfen als der Satz „wir wollen eine Event-driven Architecture". Ein typisches SaaS-Backend sieht damit meistens hybrid aus:
REST / gRPC
Client --------------------------------> API
|
| synchron
v
Account Service
|
| synchron
v
Payment Service
|
SubscriptionCreated
|
+----------+----------+
| | |
v v v
Mail Analytics CRM
Upload Service --> SQS --> Processing WorkerDarin steckt keine Ideologie. Drei Kommunikationsarten stehen nebeneinander: direkte Aufrufe, wo Antworten gebraucht werden, eine Queue, wo Arbeit verteilt wird, Events, wo eingetretene Zustandsänderungen andere Systeme interessieren. Gebaut nach der Bedeutung der jeweiligen Kommunikation, nicht nach einem einheitlichen technischen Dogma.
Event-driven ist kein Ziel
Ich würde nie ein Architekturvorhaben mit dem Ziel „wir bauen das System jetzt event-driven" starten, genauso wenig wie mit „wir bauen jetzt Microservices". Das sind Mittel. Vorher will ich wissen, welches Problem verschwinden soll. Reißen synchrone Abhängigkeiten sich gegenseitig mit? Müssen Lastspitzen abgefangen werden? Brauchen mehrere Systeme dieselben fachlichen Änderungen? Blockieren langsame Nebenprozesse einen wichtigen Request? Sollen neue Consumer unabhängig dazukommen? Dann können Events oder Queues genau die richtige Antwort sein.
Lautet das Problem aber nur „Service A braucht eine Information von Service B", dann darf die Lösung einfach sein: Service A → Service B. Ein direkter Aufruf ist dann keine technische Schuld, sondern die ehrlichste Darstellung der fachlichen Abhängigkeit, mit klaren Timeouts, sauberer Fehlerbehandlung, Observability und ohne schlechtes Gewissen.
Event-driven ist für mich nicht die fortschrittlichere Version synchroner Kommunikation. Es löst ein anderes Problem. Ich entkopple dort, wo Systeme unabhängig sein dürfen. Wo eine fachliche Abhängigkeit besteht, darf die Architektur sie auch zeigen.
Wer gerade vor dieser Entscheidung steht, findet das Vorgehen auf der Seite zur Backend-Entwicklung.

