Irgendwo in jedem Integrationsvertrag steht ein Satz wie „jede Bestellung wird genau einmal übermittelt". Er war noch nie wahr. Das Netzwerk wurde mitten in der Anfrage getrennt und der Sender wiederholte sie; die Queue lieferte nach einem Consumer-Absturz erneut zu; ein Lagermitarbeiter drückte zweimal auf den Export-Button. Duplikate sind kein Fehlerzustand - sie sind Wetter.
Idempotenzschlüssel, sorgfältig gewählt
Die Lösung ist alt und unglamourös: Jede Nachricht trägt einen Schlüssel, und die Verarbeitung wird zum No-op, wenn der Schlüssel bereits gesehen wurde. Die interessanten Entscheidungen stecken alle in der Wahl des Schlüssels.
- Natürliche Schlüssel schlagen generierte. Ein Bestellabgleich mit Schlüssel
order_id + statusübersteht es, wenn der Sender seinen Outbox neu erzeugt; eine zufällige, beim Versand erzeugte UUID nicht. - Den Schlüssel am Effekt festmachen, nicht an der Nachricht. Wenn eine Nachricht eine Rechnung erstellt und Bestand bucht, sind das zwei Effekte mit zwei Schlüsseln - sonst lässt Sie ein Teilausfall bei keinem der beiden sicher erneut versuchen.
- Schlüssel bewusst ablaufen lassen. Ein Dedup-Speicher, der ewig wächst, ist eine Zeitbombe; einer, der zu früh abläuft, führt Duplikate wieder ein. Wir setzen standardmäßig auf das 3-Fache des maximalen Retry-Horizonts des Senders.
Der Test, auf den es ankommt
Jede Integration, die wir ausliefern, besteht das, was wir intern den Doppel-Tap-Test nennen: das gesamte Nachrichtenprotokoll von gestern zweimal gegen das heutige System abspielen und den resultierenden Zustand vergleichen. Wenn sich beim zweiten Durchlauf irgendetwas verändert hat, ist die Pipeline nicht fertig.
Es ist ein brutaler Test, und er schlägt während der Entwicklung ständig fehl - bei Zeitstempeln, bei „Erstellt von"-Audit-Feldern, bei Sequenzzählern. Jeder Fehlschlag markiert eine Stelle, an der eine erneute Zustellung um 3 Uhr morgens still und leise Daten beschädigt hätte. Es ist weit günstiger, das in der Entwicklung zu entdecken als in der Produktion.