Outbox-Muster in PostgreSQL für zuverlässige API-Integrationen
Erfahren Sie das Outbox-Muster: Ereignisse in PostgreSQL speichern und zuverlässig an Drittanbieter-APIs zustellen mit Retries, Reihenfolge und Deduplizierung.

Warum Integrationen scheitern, obwohl Ihre App funktioniert
Es kommt häufig vor, dass eine Aktion in Ihrer App als „erfolgreich“ angezeigt wird, während die Integration im Hintergrund scheitert. Ihr Datenbank-Write ist schnell und verlässlich. Ein Aufruf an eine Drittanbieter-API ist es nicht. Das erzeugt zwei Welten: Ihr System sagt, die Änderung ist erfolgt, aber das externe System hat nichts davon mitbekommen.
Ein typisches Beispiel: Ein Kunde gibt eine Bestellung auf, Ihre App speichert sie in PostgreSQL und versucht dann, einen Versanddienst zu benachrichtigen. Wenn der Provider 20 Sekunden lang ausfällt oder langsam ist und Ihre Anfrage abbricht, ist die Bestellung immer noch vorhanden, aber der Versand wurde nie erstellt.
Nutzer erleben das als verwirrendes, inkonsistentes Verhalten. Fehlende Events wirken wie „nichts ist passiert“. Doppelte Events wirken wie „warum wurde mir doppelt berechnet?“. Support-Teams haben es ebenfalls schwer, weil schwer zu sagen ist, ob das Problem bei Ihrer App, im Netzwerk oder beim Partner lag.
Retries helfen, aber allein sorgen sie nicht für Korrektheit. Wenn Sie nach einem Timeout erneut senden, könnten Sie dasselbe Event zweimal schicken, weil Sie nicht wissen, ob der Partner die erste Anfrage erhalten hat. Wenn Sie in falscher Reihenfolge erneut senden, könnten Sie „Order shipped“ vor „Order paid“ schicken.
Diese Probleme entstehen oft durch normale Nebenläufigkeit: mehrere Worker, die parallel arbeiten, mehrere App-Server, die gleichzeitig schreiben, und „best-effort“-Queues, bei denen sich die Timings unter Last ändern. Die Fehlerbilder sind vorhersehbar: APIs gehen offline oder werden langsam, Netzwerke verlieren Anfragen, Prozesse stürzen zu ungünstigen Zeitpunkten ab, und Retries erzeugen Duplikate, wenn nichts Idempotenz erzwingt.
Das Outbox-Muster existiert, weil diese Fehler normal sind.
Was das Outbox-Muster einfach gesagt ist
Das Outbox-Muster ist simpel: Wenn Ihre App eine wichtige Änderung vornimmt (z. B. eine Bestellung anlegt), schreibt sie zusätzlich eine kleine „zu versendendes Event“-Zeile in eine Datenbanktabelle — in derselben Transaktion. Wenn der Datenbank-Commit erfolgreich ist, wissen Sie, dass die Geschäftsdatensätze und der Outbox-Eintrag zusammen existieren.
Anschließend liest ein separater Worker die Outbox-Tabelle und liefert diese Events an Drittanbieter-APIs. Ist eine API langsam, down oder zeitüberschreitet sie, bleibt die Hauptbenutzeranfrage trotzdem erfolgreich, weil sie nicht auf den externen Aufruf warten muss.
Das vermeidet die unangenehmen Zustände, die entstehen, wenn man die API direkt im Request-Handler aufruft:
- Die Bestellung ist gespeichert, aber der API-Aufruf schlägt fehl.
- Der API-Aufruf war erfolgreich, aber Ihre App stürzt ab, bevor die Bestellung gespeichert wird.
- Der Nutzer versucht es erneut, und Sie senden dasselbe noch einmal.
Das Outbox-Muster hilft hauptsächlich bei verlorenen Events, partiellen Fehlern (Datenbank ok, externe API nicht ok), versehentlichen Doppel-Sends und sichereren Retries (Sie können später erneut versuchen, ohne zu raten).
Es löst nicht alles. Wenn Ihre Payload falsch ist, Ihre Geschäftsregeln fehlerhaft sind oder die Drittanbieter-API die Daten ablehnt, brauchen Sie weiterhin Validierung, gutes Fehlerhandling und Möglichkeiten, fehlerhafte Events zu inspizieren und zu korrigieren.
Design einer Outbox-Tabelle in PostgreSQL
Eine gute Outbox-Tabelle ist absichtlich unspektakulär. Sie sollte einfach zu schreiben, einfach zu lesen und schwer zu missbrauchen sein.
Hier ein praktisches Basis-Schema, das Sie anpassen können:
create table outbox_events (
id bigserial primary key,
aggregate_id text not null,
event_type text not null,
payload jsonb not null,
status text not null default 'pending',
created_at timestamptz not null default now(),
available_at timestamptz not null default now(),
attempts int not null default 0,
locked_at timestamptz,
locked_by text,
meta jsonb not null default '{}'::jsonb
);
Auswahl der ID
bigserial (oder bigint) hält die Reihenfolge einfach und Indizes schnell. UUIDs sind großartig für Eindeutigkeit über Systeme hinweg, aber sie sortieren nicht nach Erstellungszeit, was Polling unvorhersehbarer und Indizes schwerer machen kann.
Ein gängiger Kompromiss ist: Behalten Sie id als bigint für die Reihenfolge bei und fügen Sie bei Bedarf eine separate event_uuid hinzu, wenn Sie eine stabile Kennung zum Teilen zwischen Services brauchen.
Wichtige Indizes
Ihr Worker wird dieselben Abfragen den ganzen Tag ausführen. Die meisten Systeme brauchen:
- Einen Index wie
(status, available_at, id), um die nächsten pending-Events in Reihenfolge zu holen. - Einen Index auf
(locked_at), wenn Sie veraltete Sperren ablaufen lassen wollen. - Einen Index wie
(aggregate_id, id), wenn Sie manchmal pro Aggregate in Reihenfolge zustellen.
Payloads klein und stabil halten
Speichern Sie nur das, was der Empfänger wirklich braucht, nicht die ganze Zeile. Fügen Sie eine explizite Version hinzu (z. B. in meta), damit Sie Felder sicher erweitern können.
Nutzen Sie meta für Routing- und Debugging-Kontext wie Tenant-ID, Correlation-ID, Trace-ID und einen Dedup-Key. Dieser zusätzliche Kontext zahlt sich später aus, wenn der Support fragen muss: „Was ist mit dieser Bestellung passiert?"
Wie man Events sicher zusammen mit dem Business-Write speichert
Die wichtigste Regel ist einfach: Schreiben Sie Geschäftsdaten und den Outbox-Event in derselben Datenbanktransaktion. Wenn die Transaktion committet, existieren beide. Wenn sie rollbackt, existiert weder das eine noch das andere.
Beispiel: Ein Kunde legt eine Bestellung an. In einer Transaktion fügen Sie die Bestellzeile, die Bestellpositionen und einen Outbox-Eintrag wie order.created hinzu. Wenn ein Schritt fehlschlägt, sollen keine „created“-Events in die Welt entweichen.
Ein Event oder mehrere?
Starten Sie, wenn möglich, mit einem Event pro Geschäftsaktion. Das ist leichter zu überblicken und günstiger zu verarbeiten. Teilen Sie in mehrere Events nur auf, wenn verschiedene Konsumenten wirklich unterschiedliche Timings oder Payloads benötigen (z. B. order.created für Fulfillment und payment.requested für Billing). Viele Events für einen Klick erhöhen Retries, Ordnungsprobleme und das Handling von Duplikaten.
Welche Payload speichern?
Meist wählen Sie zwischen:
- Snapshot: Speichern Sie Schlüsselfelder so, wie sie beim Zeitpunkt der Aktion waren (Order-Total, Währung, Customer-ID). Das vermeidet zusätzliche Reads später und macht die Nachricht stabil.
- Referenz-ID: Speichern Sie nur die Order-ID und lassen den Worker die Details später laden. Das hält die Outbox klein, erhöht aber Reads und kann problematisch werden, wenn die Bestellung später bearbeitet wird.
Ein praktischer Mittelweg ist Identifikatoren plus ein kleiner Snapshot kritischer Werte. Das hilft Empfängern, schnell zu handeln und Ihnen beim Debuggen.
Halten Sie die Transaktionsgrenze eng. Rufen Sie niemals Drittanbieter-APIs innerhalb derselben Transaktion auf.
Zustellung von Events an Drittanbieter-APIs: die Worker-Schleife
Sobald Events in der Outbox liegen, brauchen Sie einen Worker, der sie liest und die Drittanbieter-API aufruft. Das ist der Teil, der das Muster in eine verlässliche Integration verwandelt.
Polling ist meist die einfachste Option. LISTEN/NOTIFY kann die Latenz verringern, bringt aber zusätzliche Komplexität und braucht einen Fallback, wenn Benachrichtigungen verloren gehen oder der Worker neu startet. Für die meisten Teams ist konstantes Polling mit kleinen Batches leichter zu betreiben und zu debuggen.
Zeilen sicher claimen
Der Worker sollte Zeilen claimen, damit niemals zwei Worker dasselbe Event gleichzeitig verarbeiten. In PostgreSQL ist ein üblicher Ansatz, ein Batch mit Row-Locks und SKIP LOCKED zu selektieren und diese dann als in Arbeit zu markieren.
Ein praktischer Status-Flow ist:
pending: bereit zum Sendenprocessing: von einem Worker gesperrt (nutzelocked_byundlocked_at)sent: erfolgreich zugestelltfailed: nach maximalen Versuchen gestoppt (oder zur manuellen Prüfung verschoben)
Halten Sie Batches klein, um die Datenbank zu schonen. Ein Batch von 10 bis 100 Zeilen, der alle 1 bis 5 Sekunden läuft, ist ein guter Ausgangspunkt.
Wenn ein Call erfolgreich ist, markieren Sie die Zeile als sent. Wenn er fehlschlägt, inkrementieren Sie attempts, setzen available_at in die Zukunft (Backoff), löschen die Sperre und geben sie zurück an pending.
Nützliche Logs (ohne Secrets zu leaken)
Gute Logs machen Fehler handhabbar. Loggen Sie die Outbox-id, den Event-Typ, den Zielnamen, die Attempt-Anzahl, Dauer und HTTP-Status oder Fehlerklasse. Vermeiden Sie Request-Bodies, Auth-Header und komplette Responses. Wenn Sie Korrelation brauchen, speichern Sie eine sichere Request-ID oder einen Hash statt roher Payload-Daten.
Ordnungsregeln, die in echten Systemen funktionieren
Viele Teams starten mit „sende Events in der gleichen Reihenfolge, in der wir sie erstellt haben“. Das Problem ist: „die gleiche Reihenfolge“ ist selten global. Wenn Sie eine globale Queue erzwingen, kann ein einzelner langsamer Kunde oder eine fehlerhafte API alle anderen blockieren.
Eine praktikable Regel ist: Erhalte die Reihenfolge pro Gruppe, nicht systemweit. Wählen Sie einen Gruppierungsschlüssel, der zur Sichtweise der Außenwelt passt, z. B. customer_id, account_id oder ein aggregate_id wie order_id. Garantieren Sie dann die Reihenfolge innerhalb jeder Gruppe, während viele Gruppen parallel bearbeitet werden.
Parallele Worker ohne Reihenfolge zu brechen
Führen Sie mehrere Worker aus, aber stellen Sie sicher, dass nie zwei Worker gleichzeitig dieselbe Gruppe verarbeiten. Üblich ist: immer das früheste ungesendete Event für eine gegebene aggregate_id zustellen und Parallelität nur über verschiedene Aggregates zu erlauben.
Halten Sie die Claim-Regeln einfach:
- Liefere nur das früheste pending-Event pro Gruppe.
- Erlaube Parallelität über Gruppen, nicht innerhalb einer Gruppe.
- Claim ein Event, sende es, aktualisiere den Status und mache weiter.
Wenn ein Event alles blockiert
Irgendwann wird ein „Poison“-Event stundenlang fehlschlagen (falsche Payload, widerrufenes Token, Provider-Ausfall). Wenn Sie strikt Reihenfolge pro Gruppe durchsetzen, müssen spätere Events dieser Gruppe warten, aber andere Gruppen sollen weiterlaufen.
Ein praktikabler Kompromiss ist, Retries pro Event zu begrenzen. Danach markieren Sie es als failed und pausieren nur diese Gruppe, bis jemand die Ursache behebt. So wird ein gebrochener Kunde nicht alle anderen verlangsamen.
Retries ohne es schlimmer zu machen
Retries entscheiden, ob Ihr Outbox-Setup verlässlich oder laut wird. Ziel: Erneut versuchen, wenn es wahrscheinlich ist, dass es klappt, und schnell aufhören, wenn es nicht so ist.
Verwenden Sie exponentielles Backoff und eine harte Obergrenze. Beispiel: 1 Minute, 2 Minuten, 4 Minuten, 8 Minuten, dann stoppen (oder weiter mit maximaler Verzögerung z. B. 15 Minuten). Setzen Sie immer eine Maximalanzahl an Versuchen, damit ein fehlerhaftes Event das System nicht ewig blockiert.
Nicht jeder Fehler soll erneut versucht werden. Regeln:
- Retry: Netzwerk-Timeouts, Verbindungsabbrüche, DNS-Probleme und HTTP 429 oder 5xx-Antworten.
- Kein Retry: HTTP 400 (bad request), 401/403 (Auth-Probleme), 404 (falscher Endpoint) oder Validierungsfehler, die Sie vor dem Senden erkennen können.
Speichern Sie den Retry-Status in der Outbox-Zeile. Erhöhen Sie attempts, setzen Sie available_at auf den nächsten Versuch und speichern Sie eine kurze, sichere Fehlerzusammenfassung (Statuscode, Fehlerklasse, gekürzte Nachricht). Speichern Sie keine kompletten Payloads oder sensible Daten in den Fehlerfeldern.
Rate-Limits brauchen besondere Behandlung. Bei HTTP 429 respektieren Sie Retry-After, wenn vorhanden. Ansonsten backoffen Sie aggressiver, um ein Retry-Sturm zu vermeiden.
Deduplizierung und Idempotenz-Grundlagen
Wenn Sie verlässliche API-Integrationen bauen, gehen Sie davon aus, dass dasselbe Event mehrfach gesendet werden kann. Ein Worker kann nach dem HTTP-Call abstürzen, bevor er Erfolg speichert. Ein Timeout kann einen Erfolg verbergen. Ein Retry kann sich mit einem langsamen ersten Versuch überlappen. Das Outbox-Muster reduziert verlorene Events, verhindert aber nicht automatisch Duplikate.
Der sicherste Ansatz ist Idempotenz: Wiederholte Zustellungen führen zum selben Ergebnis wie eine einmalige Zustellung. Wenn Sie eine Drittanbieter-API aufrufen, fügen Sie einen Idempotency-Key hinzu, der für dieses Event und dieses Ziel stabil bleibt. Viele APIs unterstützen einen Header; wenn nicht, legen Sie den Key in den Request-Body.
Ein einfacher Key ist Ziel plus Event-ID. Für ein Event mit ID evt_123 nutzen Sie z. B. destA:evt_123.
Auf Ihrer Seite verhindern Sie doppelte Sends durch ein Outbound-Delivery-Log und erzwingen eine Unique-Rule wie (destination, event_id). Selbst wenn zwei Worker konkurrieren, kann nur einer den „wir senden das“-Eintrag erstellen.
Webhooks duplizieren ebenfalls
Wenn Sie Webhook-Callbacks empfangen (z. B. „delivery confirmed“), behandeln Sie sie gleich. Provider retryen und senden dieselbe Payload mehrfach. Speichern Sie verarbeitete Webhook-IDs oder berechnen Sie einen stabilen Hash aus der Provider-Message-ID und lehnen Wiederholungen ab.
Wie lange Daten behalten?
Bewahren Sie Outbox-Zeilen, bis Sie Erfolg gespeichert haben (oder einen finalen Fehler akzeptieren). Bewahren Sie Delivery-Logs länger auf — sie sind Ihr Audit-Trace, wenn jemand fragt: „Haben wir das gesendet?"
Gängige Vorgehensweise:
- Outbox-Zeilen: löschen oder archivieren nach Erfolg plus kurzem Sicherheitsfenster (Tage).
- Delivery-Logs: mehrere Wochen oder Monate aufbewahren, je nach Compliance und Support-Bedarf.
- Idempotency-Keys: mindestens so lange behalten, wie Retries möglich sind (und länger für Webhook-Duplikate).
Schritt-für-Schritt: Outbox-Muster implementieren
Entscheiden Sie, was Sie publizieren. Halten Sie Events klein, fokussiert und leicht wiederspielbar. Eine gute Regel: eine Geschäfts-Info pro Event, mit genug Daten, damit der Empfänger handeln kann.
Fundament bauen
Wählen Sie klare Event-Namen (z. B. order.created, order.paid) und versionieren Sie Ihr Payload-Schema (z. B. v1, v2). Versionierung erlaubt Feld-Erweiterungen ohne alte Konsumenten zu brechen.
Erstellen Sie Ihre PostgreSQL-Outbox-Tabelle und fügen Sie Indizes für die wichtigsten Worker-Abfragen hinzu, vor allem (status, available_at, id).
Aktualisieren Sie Ihren Write-Flow so, dass die Geschäftsänderung und das Outbox-Insert in derselben DB-Transaktion passieren. Das ist die Kern-Garantie.
Zustellung und Kontrolle hinzufügen
Ein einfacher Implementierungsplan:
- Definieren Sie Event-Typen und Payload-Versionen, die Sie langfristig unterstützen.
- Erstellen Sie die Outbox-Tabelle und Indizes.
- Fügen Sie beim Hauptdaten-Change eine Outbox-Zeile ein.
- Bauen Sie einen Worker, der Zeilen claimed, an die Drittanbieter-API sendet und den Status aktualisiert.
- Fügen Sie Retry-Planung mit Backoff und einen
failed-Status hinzu, wenn Versuche erschöpft sind.
Fügen Sie grundlegende Metriken hinzu, damit Sie Probleme früh bemerken: Lag (Alter des ältesten ungesendeten Events), Send-Rate und Fehlerquote.
Ein einfaches Beispiel: Order-Events an externe Services senden
Ein Kunde legt in Ihrer App eine Bestellung an. Zwei Dinge müssen außerhalb Ihres Systems passieren: Billing muss die Karte belasten und der Versanddienst einen Versand anlegen.
Mit dem Outbox-Muster rufen Sie diese APIs nicht im Checkout-Request auf. Stattdessen speichern Sie die Bestellung und eine Outbox-Zeile in derselben PostgreSQL-Transaktion, sodass Sie nie in die Lage „Bestellung gespeichert, aber keine Benachrichtigung“ (oder umgekehrt) kommen.
Eine typische Outbox-Zeile für ein Order-Event enthält aggregate_id (die Order-ID), event_type wie order.created und eine JSONB-Payload mit Summen, Items und Versanddetails.
Ein Worker holt sich dann pending-Zeilen und ruft die externen Dienste auf (entweder in definierter Reihenfolge oder indem er separate Events wie payment.requested und shipment.requested emittiert). Wenn ein Provider down ist, protokolliert der Worker den Versuch, plant den nächsten Versuch durch Setzen von available_at weiter in die Zukunft und macht weiter. Die Bestellung bleibt vorhanden und das Event wird später erneut versucht, ohne neue Checkouts zu blockieren.
Ordnung ist üblicherweise „pro Bestellung“ oder „pro Kunde“. Stellen Sie sicher, dass Events mit derselben aggregate_id nacheinander verarbeitet werden, damit order.paid nie vor order.created ankommt.
Deduplizierung verhindert doppelte Belastungen oder doppelte Sendungen. Senden Sie einen Idempotency-Key, wenn der Drittanbieter das unterstützt, und führen Sie ein Delivery-Record pro Ziel, damit ein Retry nach Timeout keine zweite Aktion auslöst.
Schnelle Checks vor dem Live-Gang
Bevor Sie einer Integration erlauben, Geld zu bewegen, Kunden zu benachrichtigen oder Daten zu synchronisieren, testen Sie die Ränder: Abstürze, Retries, Duplikate und mehrere Worker.
Checks, die häufige Fehler aufdecken:
- Bestätigen Sie, dass die Outbox-Zeile in derselben Transaktion wie die Geschäftsänderung erstellt wird.
- Verifizieren Sie, dass der Sender sicher in mehreren Instanzen läuft. Zwei Worker dürfen nicht dasselbe Event gleichzeitig senden.
- Wenn Reihenfolge wichtig ist, definieren und enforce die Regel mit einem stabilen Schlüssel in einem Satz.
- Entscheiden Sie für jedes Ziel, wie Sie Duplikate verhindern und wie Sie beweisen, dass „wir es gesendet haben“.
- Definieren Sie den Exit: Nach N Versuchen das Event zu
failedschieben, die letzte Fehlerzusammenfassung behalten und eine einfache Reprocess-Aktion vorsehen.
Realitätscheck: Stripe könnte eine Anfrage akzeptieren, aber Ihr Worker stürzt ab, bevor er den Erfolg speichert. Ohne Idempotenz kann ein Retry eine doppelte Aktion auslösen. Mit Idempotenz plus gespeicherter Delivery-Record wird der Retry sicher.
Nächste Schritte: rollout ohne Ihre App zu stören
Der Rollout entscheidet oft über Erfolg oder Stillstand von Outbox-Projekten. Beginnen Sie klein, damit Sie reales Verhalten sehen, ohne Ihre ganze Integrationsschicht zu riskieren.
Starten Sie mit einer Integration und einem Event-Typ. Zum Beispiel nur order.created an einen einzelnen Vendor senden, während alles andere vorerst bleibt wie es ist. So erhalten Sie eine saubere Basislinie für Durchsatz, Latenz und Fehlerquoten.
Machen Sie Probleme früh sichtbar. Dashboard und Alerts für Outbox-Lag (wie viele Events warten und wie alt das älteste ist) und Fehlerquote (wie viele in Retry stecken) helfen. Wenn Sie in 10 Sekunden beantworten können: „Sind wir gerade im Rückstand?“, erkennen Sie Probleme, bevor Nutzer sie bemerken.
Haben Sie einen sicheren Reprocess-Plan, bevor der erste Vorfall eintritt. Entscheiden Sie, was „reprocess“ bedeutet: dieselbe Payload nochmals senden, die Payload aus aktuellen Daten neu bauen oder manuell prüfen lassen. Dokumentieren Sie, welche Fälle sicher erneut gesendet werden können und welche menschliche Kontrolle brauchen.
Wenn Sie das mit einer No-Code-Plattform wie AppMaster (appmaster.io) bauen, gilt dieselbe Struktur: Schreiben Sie Ihre Geschäftsdaten und eine Outbox-Zeile zusammen in PostgreSQL und lassen Sie einen separaten Backend-Prozess zustellen, retryen und Events als sent oder failed markieren.
FAQ
Verwende das Outbox-Muster, wenn eine Nutzeraktion deine Datenbank aktualisiert und gleichzeitig Arbeit in einem anderen System auslösen muss. Es ist besonders nützlich, wenn Timeouts, instabile Netzwerke oder Ausfälle von Drittanbietern dazu führen können, dass etwas „bei uns gespeichert“ ist, aber „bei ihnen fehlt”.
Das gleichzeitige Schreiben der Geschäftszeile und der Outbox-Zeile in einer Datenbanktransaktion gibt dir eine einfache Garantie: Entweder existieren beide oder keine. Das verhindert Teilfehler wie „API-Aufruf erfolgreich, aber die Bestellung wurde nicht gespeichert“ oder „Bestellung gespeichert, aber der API-Aufruf hat nie stattgefunden“.
Als gutes Default-Set empfiehlt sich: id, aggregate_id, event_type, payload, status, created_at, available_at, attempts sowie Sperrfelder wie locked_at und locked_by. Damit lassen sich Senden, Retry-Planung und sichere Nebenläufigkeit einfach handhaben, ohne die Tabelle zu überladen.
Eine sinnvolle Basis ist ein Index auf (status, available_at, id), damit Worker schnell die nächsten versandbereiten Events in Reihenfolge abfragen können. Zusätzliche Indizes nur hinzufügen, wenn du wirklich danach abfragst — zu viele Indizes verlangsamen Inserts.
Polling ist für die meisten Teams die einfachste und vorhersagbarste Lösung. Starte mit kleinen Batches und kurzen Intervallen, und optimiere dann nach Last und Lag; eine einfache Schleife ist leichter zu debuggen, wenn etwas schiefgeht.
Claim die Zeilen mit Row-Level-Locks, damit zwei Worker nicht gleichzeitig dasselbe Event senden — typischerweise mit SKIP LOCKED. Markiere die Zeile dann als processing mit Zeitstempel und Worker-ID, sende das Event und markiere es anschließend als sent oder gib es mit zukünftiger available_at wieder frei.
Verwende exponentiellen Backoff mit einer harten Maximalanzahl an Versuchen und retry nur bei wahrscheinlichen temporären Fehlern. Timeouts, Netzwerkfehler und HTTP 429/5xx sind gute Kandidaten für Retries; Validierungsfehler und die meisten 4xx-Antworten sollten als final behandelt werden, bis die Daten oder Konfiguration korrigiert sind.
Nein — Du musst weiterhin mit Duplikaten rechnen, z. B. wenn ein Worker nach dem HTTP-Aufruf abstürzt, bevor er den Erfolg speichert. Nutze einen Idempotency-Key, der pro Ziel und Event stabil ist, und führe ein Delivery-Log mit einer eindeutigen Einschränkung wie (destination, event_id), damit selbst bei Race-Conditions nicht zweimal dieselbe Aktion ausgelöst wird.
Bewahre Reihenfolge innerhalb einer Gruppe, nicht global. Verwende einen Gruppierungsschlüssel wie aggregate_id oder customer_id, verarbeite pro Gruppe jeweils nur ein Event gleichzeitig und erlaube Parallelität über verschiedene Gruppen hinweg — so blockiert ein langsamer Kunde nicht das ganze System.
Markiere das Event nach einer maximalen Anzahl von Versuchen als failed, halte eine kurze, sichere Fehlerzusammenfassung und stoppe die Verarbeitung späterer Events derselben Gruppe, bis die Ursache behoben ist. So begrenzt du den Schaden und verhinderst endlose Retry-Schleifen, während andere Gruppen weiterlaufen können.


