Ein Use Case kann fachlich in einem Satz beschrieben sein und im Betrieb trotzdem drei unterschiedliche Fehlerzustände erzeugen: Eine Anfrage läuft ab, ein Ereignis wird erneut zugestellt, oder eine Teilaktion gelingt ohne ihre Folgeaktion. Meine Position: Für einen zusammenhängenden Nutzerauftrag ist synchrone HTTP-Orchestrierung der bessere Standard. Ereignisbasierte Choreografie gewinnt erst, wenn Arbeit tatsächlich unabhängig weiterlaufen darf – und das Team ihre Zustell- und Wiederholungsfehler beherrscht.
Synchrone Orchestrierung gewinnt, solange der Auftrag eine gemeinsame Antwort braucht
Bei einem Auftrag wie „Reservierung anlegen und Verfügbarkeit bestätigen“ klingt ein Ereignis für jeden fachlichen Schritt elegant. Für den Betrieb ist zunächst eine andere Frage entscheidend: Muss der aufrufende Client jetzt wissen, ob die Reservierung angenommen wurde? Falls ja, sollte ein Orchestrator die nötigen Dienste über HTTP aufrufen und eine eindeutige Antwort liefern, weil ein verteiltes Ereignisgeflecht die Entscheidung nicht schneller oder eindeutiger macht.
Die Gegenüberstellung ist konkret: Synchrone HTTP-Orchestrierung gewinnt bei kurzen, voneinander abhängigen Schritten und kostet gekoppelte Verfügbarkeit sowie ein gemeinsames Latenzbudget. Ereignisbasierte Choreografie gewinnt bei entkoppelbaren Folgeschritten, unterschiedlichen Verarbeitungsgeschwindigkeiten oder bewusst verzögerter Fertigstellung und kostet Brokerbetrieb, Idempotenz, Rückstände und schwerer nachvollziehbare Zwischenzustände. Ein einzelner Use Case kann beide Formen enthalten; die Grenze gehört dorthin, wo der Client keine sofortige Antwort mehr über das Ergebnis benötigt.
Die fachliche Benennung in Software Use Cases in der IT Entwicklung ist ein brauchbarer Ausgangspunkt, aber aus einem fachlichen Schritt folgt noch keine eigene Nachricht. Wenn die Bestätigung der Verfügbarkeit Voraussetzung für „angenommen“ ist, gehört sie vor die Antwort. Eine spätere Benachrichtigung kann dagegen asynchron laufen, weil ihr Ausfall die bereits bestätigte Reservierung nicht rückwirkend ungültig macht.
Für die synchrone Variante würde ich den Vertrag mit OpenAPI 3.1 beschreiben und die Antworten nach HTTP-Semantik gestalten: Ein erfolgreicher Abschluss erhält eine definitive Antwort; 202 Accepted bleibt einem Auftrag vorbehalten, dessen Ergebnis noch aussteht. Für einen tatsächlich abgeschlossenen, aber fachlich kollidierenden Versuch ist 409 Conflict aussagekräftiger als ein pauschales 500, weil Clients sonst einen erwartbaren Zustand wie einen Serverdefekt erneut versuchen. Ein vom Client gelieferter Idempotency-Key verhindert bei einem verlorenen Antwortpaket doppelte Reservierungen, sofern der Server Schlüssel und Ergebnis dauerhaft gemeinsam speichert.
Als einzustellenden Startwert würde ich dem gesamten synchronen Pfad ein Budget von 800 Millisekunden geben und es anschließend anhand realer Traces ändern. Einzelne Dienste dürfen dieses Budget nicht jeweils vollständig verbrauchen, weil sich Wartezeiten entlang der Aufrufkette addieren. Ein Timeout ist dabei kein Beweis, dass die Gegenstelle nichts getan hat: Genau deshalb braucht der anschließende Retry denselben Idempotency-Key.
Ereignisse gewinnen erst, wenn ein ausstehender Zustand fachlich erlaubt ist
Ein Ereignis ist sinnvoll, wenn ein abgeschlossener Kernschritt weitere Arbeit auslöst, deren Ergebnis den ursprünglichen Auftrag nicht mehr blockieren soll. Nach einer bestätigten Reservierung kann etwa ein Bestätigungsdokument entstehen. Dauert dessen Erzeugung länger, schützt eine Queue den Antwortpfad vor dieser Laufzeit; sie garantiert allerdings weder sofortige Bearbeitung noch genau eine Ausführung.
Ich würde nicht jeden Methodenaufruf eines Use Cases als Kafka-Ereignis veröffentlichen, weil aus einer überschaubaren Aufrufkette sonst mehrere dauerhaft zu betreibende Fehler- und Versionsgrenzen werden. Besonders problematisch ist das bei zwei Schritten, die gemeinsam Erfolg oder Misserfolg bestimmen: Erhält der Client bereits „fertig“, während der zweite Consumer noch arbeitet, stimmt die Antwort nicht mehr mit dem tatsächlichen Zustand überein. Ist Verzögerung fachlich akzeptiert, muss die API stattdessen einen ausstehenden Auftrag samt abfragbarer Kennung zurückgeben.
RabbitMQ kann für Arbeitsaufträge mit expliziter Bestätigung und erneuter Zustellung passen; Kafka passt eher, wenn mehrere Consumer denselben dauerhaften Ereignisstrom unabhängig verarbeiten sollen, weil Retention und Consumer-Offsets dort zum Betriebsmodell gehören. Keines der beiden Systeme löst die Kopplung zwischen einer Datenbankänderung und einer externen Veröffentlichung von allein. Eine PostgreSQL-Outbox schließt diese Lücke: Der Dienst schreibt fachlichen Zustand und ausgehendes Ereignis in einer Transaktion, ein separater Prozess veröffentlicht die Outbox-Einträge. Der Preis sind zusätzliche Tabellenpflege, ein Publisher und die Beobachtung seines Rückstands.
Auf der Empfängerseite muss Wiederholung der Normalfall sein. Die folgende Anweisung läuft in PostgreSQL und zeigt das kleinste belastbare Prinzip: Ein eindeutiger Ereignisschlüssel und die fachliche Änderung liegen in derselben Datenbankanweisung. Wird die WITH-Anweisung erneut ausgeführt, entsteht keine zweite Reservierung.
CREATE TEMP TABLE inbox (event_id text PRIMARY KEY);
CREATE TEMP TABLE reservations (event_id text PRIMARY KEY, order_id text NOT NULL);
WITH accepted AS (
INSERT INTO inbox(event_id) VALUES ('evt-42')
ON CONFLICT DO NOTHING RETURNING event_id
)
INSERT INTO reservations(event_id, order_id)
SELECT event_id, 'order-7' FROM accepted;
SELECT count(*) AS reservations FROM reservations;
Das Beispiel schützt ausschließlich Änderungen in dieser Datenbank. Ein daneben ausgeführter HTTP-Aufruf wird dadurch nicht atomar; er benötigt einen eigenen Idempotenzvertrag oder eine explizite Kompensation. Auch Kafkas Producer-Einstellungen enable.idempotence=true und acks=all ersetzen diese Consumer-Logik nicht, weil sie keine beliebigen externen Seiteneffekte genau einmal ausführen.
Die Entscheidung fällt an Fehlermodi, nicht am Architekturdiagramm
Für den produktiven Betrieb würde ich beide Ansätze an denselben Ausfällen prüfen. Was passiert, wenn die fachliche Änderung gelingt und die Antwort verloren geht? Was passiert, wenn ein Prozess unmittelbar nach dem Commit beendet wird? Wie erkennt das Team, dass Arbeit nur langsam statt gar nicht voranschreitet? Die Antworten unterscheiden sich stärker als die Diagramme, weil HTTP vor allem unbekannte Ergebnisse nach Timeouts erzeugt, während ein Broker zusätzlich Rückstände und erneute Zustellungen sichtbar macht.
Für einen synchronen Pfad gehören Request-ID, Idempotency-Key und ein End-to-End-Trace zusammen. W3C Trace Context transportiert den Trace über Dienstgrenzen; OpenTelemetry instrumentiert die Aufrufe, und Jaeger kann die zeitliche Abfolge anzeigen. Die wichtigste Metrik ist nicht allein die mittlere Antwortzeit: Das p95 der erfolgreichen Antworten, Timeout-Raten und die Zahl unklarer Ergebnisse zeigen, ob der Vertrag für Clients tragfähig bleibt. In einem ausdrücklich hypothetischen Lasttest wären 420 Millisekunden am p95 unauffällig, während eine kleine Gruppe von Anfragen über dem Client-Timeout trotzdem doppelte Versuche auslösen könnte; deshalb müssen Traces und Retry-Zähler gemeinsam betrachtet werden.
Für einen Ereignispfad kommen Consumer-Lag, Alter des ältesten unbearbeiteten Ereignisses, Wiederholungsrate und Größe der Dead-Letter-Queue hinzu. Prometheus kann diese Werte einsammeln und Grafana sie neben der Erfolgsrate des Use Cases darstellen. Ein bloßer Lag in Nachrichten ist bei schwankender Eingangsrate schwer zu beurteilen; das Alter der ältesten offenen Arbeit beantwortet direkter, wie lange ein Auftrag bereits wartet. Als lokale Alarmgrenze zum Nachjustieren könnten fünf Minuten für ein Bestätigungsdokument sinnvoll sein, sofern das Team vorher festlegt, welcher fachliche Schaden nach dieser Wartezeit entsteht.
CloudEvents 1.0 gibt Ereignissen Felder wie id, type und source; diese Metadaten erleichtern Zuordnung und Deduplizierung, ersetzen aber kein versioniertes fachliches Schema. Wenn ein Consumer ein neues Pflichtfeld voraussetzt, bevor alle Publisher es senden, wird aus einer kleinen Änderung eine Produktionsstörung. Neue Felder sollten deshalb zunächst optional lesbar sein, und Contract-Tests müssen sowohl alte als auch neue Beispiele enthalten.
In Kubernetes verdient der Prozessabbruch besondere Aufmerksamkeit. Die Kubernetes-Dokumentation nennt terminationGracePeriodSeconds: 30 als Standardwert; reicht diese Zeit nicht, um laufende Arbeit zu beenden oder sicher erneut zustellbar zu machen, kann ein Rollout Aufträge unterbrechen. Eine Readiness-Probe sollte einen überlasteten synchronen Dienst aus dem Verkehr nehmen, während ein Consumer vor dem Beenden keine neuen Nachrichten mehr annehmen und unbestätigte Arbeit dem Broker überlassen sollte. Beides muss im Staging durch einen tatsächlichen Pod-Abbruch geprüft werden, weil eine erfolgreiche Probe keine korrekte Abbruchlogik beweist.
Ein Pilot sollte zuerst die unangenehme Grenze testen
Die Entscheidung lässt sich mit einem kleinen Produktionskandidaten treffen, ohne sofort die gesamte Anwendung umzubauen. Wählen Sie einen Use Case mit einer klaren Antwort und genau einem optionalen Folgeschritt. Implementieren Sie den Kern synchron und den Folgeschritt zunächst nur dann asynchron, wenn dessen Verzögerung fachlich erlaubt ist. Diese Trennung erzeugt eine überprüfbare Grenze, statt aus Stilgründen eine zusätzliche Zustellstrecke einzuführen.
Vor dem Rollout würde ich drei Versuche automatisieren: dieselbe Anfrage mit identischem Idempotency-Key wiederholen, den Dienst unmittelbar nach dem Datenbank-Commit beenden und einen Consumer während der Verarbeitung neu starten. Der Test ist erst bestanden, wenn die fachliche Änderung genau einmal sichtbar ist oder der offene Zustand zuverlässig erneut verarbeitet wird. Ein grüner HTTP-Status allein genügt nicht, weil er weder verlorene Antworten noch spätere Consumer-Fehler belegt.
Anwendungen und Use Cases in der Softwareentwicklung verbindet Abläufe mit ihren Anwendungen; für meine Betriebsentscheidung trenne ich zusätzlich „angenommen“, „in Bearbeitung“ und „abgeschlossen“. Diese Zustände gehören in API-Antworten und Dashboards, weil ein Bereitschaftsdienst sonst aus Broker-Metriken erraten muss, was ein betroffener Client gerade sieht.
Ein vorläufiges SLO von 99,9 Prozent für definitive Antworten ist nur dann nützlich, wenn „definitiv“ ausdrücklich einen bestätigten Erfolg oder einen klaren fachlichen Fehler meint. 202 Accepted darf nicht als abgeschlossener Erfolg gezählt werden, weil es lediglich die Annahme der Arbeit bestätigt. Bei asynchroner Verarbeitung braucht das Endergebnis daher eine eigene Messung: Anteil fristgerecht abgeschlossener Aufträge und Alter der weiterhin offenen Aufträge.
Wenn der Pilot zeigt, dass der synchrone Kern regelmäßig an einer langsamen, fachlich unabhängigen Aktion wartet, würde ich genau diese Aktion auslagern. Zeigt er stattdessen doppelte Wirkungen nach Retries, würde ich zuerst die Idempotenz reparieren und keinen Broker hinzufügen, weil zusätzliche Zustellung denselben Fehler häufiger sichtbar machen kann.
Der erste Schritt ist ein Abbruchtest am bestehenden Use Case
Nehmen Sie den nächsten produktiven Use Case und unterbrechen Sie ihn einmal direkt nach seiner dauerhaften Änderung: per Prozessabbruch, nicht per Mock. Prüfen Sie anschließend Client-Antwort, gespeicherten Zustand und Wiederholungsverhalten. Bleibt das Ergebnis unklar, definieren Sie zunächst Idempotency-Key und Statusabfrage. Erst wenn diese Grenze verlässlich funktioniert, lohnt die Entscheidung, welcher tatsächlich unabhängige Schritt künftig über ein Ereignis laufen soll.





