Ein Payment-Request wird beim Client wegen eines Timeouts erneut gesendet. Ein Job-Worker startet nach einem Crash denselben Auftrag noch einmal. Oder ein Browser feuert bei schlechter Verbindung ein Formular doppelt ab. Solche Situationen sind keine Ausnahme, sondern Normalbetrieb in verteilten Systemen. Wer hier nicht vorsorgt, baut sich schleichend Fehlerbilder ein: doppelte Bestellungen, zweimal verschickte E-Mails, überzählige Ressourcen oder falsch aggregierte Zählerstände.
Ein stabiler Ausweg ist ein Design, das Wiederholungen toleriert. Idempotenz bedeutet dabei: derselbe Request kann mehrfach ankommen, ohne dass dadurch ein zusätzlicher Effekt entsteht. Das Konzept ist älter als Web-APIs, wird aber in modernen Architekturen (Cloud, Microservices, asynchrone Verarbeitung) noch wichtiger.
Warum Requests doppelt ankommen (und warum das normal ist)
Retries auf vielen Ebenen
Wiederholungen entstehen nicht nur im Code. Load Balancer, API-Gateways, SDKs, Message-Broker-Clients oder HTTP-Stacks können automatisch erneut senden, wenn eine Verbindung abbricht oder ein Timeout eintritt. Auch menschliches Verhalten (Doppelklick, Refresh) führt zu Duplikaten. Ein Backend darf deshalb nicht implizit annehmen, dass jede Anfrage genau einmal ankommt.
At-least-once ist häufig die Realität
Viele Systeme liefern Ereignisse „mindestens einmal“. Das ist oft eine bewusste Designentscheidung: „at-least-once“ ist leichter robust zu betreiben als „exactly-once“ über Prozess- und Systemgrenzen hinweg. Daraus folgt: Die Anwendungsschicht muss Duplikate beherrschen, statt sie wegzuwünschen.
Was Idempotenz in HTTP wirklich heißt
Methoden-Semantik vs. fachlicher Effekt
HTTP kennt idempotente Methoden (klassisch GET, PUT, DELETE). Das ist eine hilfreiche Leitplanke, ersetzt aber keine fachliche Betrachtung. Ein DELETE kann technisch idempotent wirken (zweites DELETE liefert wieder 204), aber fachlich trotzdem Nebenwirkungen haben, wenn z. B. jedes Löschen ein Audit-Event oder eine Benachrichtigung auslöst. Umgekehrt kann ein POST idempotent umgesetzt werden, wenn das Backend Duplikate erkennt und denselben Effekt nicht erneut ausführt.
Erwartbares Antwortverhalten
Idempotentes Verhalten umfasst nicht nur den Zustand, sondern auch die Rückgabe. Bei einem wiederholten Request sollte die API möglichst dieselbe Repräsentation liefern wie beim ersten erfolgreichen Durchlauf (Statuscode, Body, Location, ggf. Error-Details). Das erleichtert Client-Logik und reduziert „Heisenbugs“ im Frontend.
Idempotency-Key: das zentrale Muster für write-Requests
Was ein Idempotency-Key leisten muss
Ein Idempotency-Key ist eine eindeutige Kennung pro fachlicher Operation, die der Client mitsendet. Das Backend speichert zu diesem Key das Ergebnis (oder zumindest eine Referenz darauf) und kann bei einem erneuten Request erkennen: Diese Operation wurde bereits verarbeitet oder läuft gerade.
Wichtig ist die Granularität: Der Key darf nicht „pro Nutzer“ oder „pro Session“ sein, sondern pro Operation. Für „Bestellung anlegen“ ist das eine andere Operation als „Adresse ändern“.
Was im Backend gespeichert wird
Praktisch bewährt hat sich eine kleine Tabelle/Collection für Idempotency-Einträge mit:
- Key (eindeutig, per Unique Constraint abgesichert)
- Scope (z. B. Customer-ID oder API-Client-ID), damit Keys nicht global kollidieren
- Request-Fingerprint (z. B. Hash aus relevanten Feldern), um Key-Missbrauch zu erkennen
- Status (in_progress, completed, failed)
- Response-Payload oder Referenz (z. B. Resource-ID + Statuscode)
- TTL/Expiration (Aufbewahrungszeit, je nach Domain)
Der Request-Fingerprint verhindert, dass ein Client denselben Key mit anderem Inhalt wiederverwendet und damit inkonsistente Ergebnisse provoziert. In so einem Fall ist eine 409 (Conflict) oder 422 (Unprocessable Entity) oft sinnvoll, je nachdem wie die API Fehler klassifiziert.
Datenbank-Strategien: Duplikate verhindern, ohne zu locken
Unique Constraints als Sicherheitsnetz
Die erste Schutzschicht sitzt oft in der Datenbank: Wenn eine Bestellung eine externe Referenz hat (z. B. client_order_id), sollte darauf ein Unique Constraint liegen. Damit wird aus einer potenziell doppelten Insert-Operation deterministisch: genau ein Insert gewinnt, der zweite wird abgefangen und kann auf die bestehende Ressource auflösen.
Atomic Insert-or-Get statt verteiltem Locking
Eine robuste Implementierung vermeidet lange Sperren. Typischer Ablauf:
- Versuch, einen Idempotency-Eintrag mit Status in_progress atomar anzulegen (Unique Constraint auf Key+Scope).
- Wenn erfolgreich: fachliche Operation ausführen, Ergebnis speichern, Status auf completed setzen.
- Wenn Eintrag existiert: gespeichertes Ergebnis zurückgeben oder – bei in_progress – kurz warten/pollen bzw. 409/202 zurückgeben (je nach API-Design).
Dadurch entsteht ein klarer Single-Writer-Pfad pro Operation, ohne dass globale Locks notwendig sind.
Typische Stolperfallen in verteilten Systemen
Nebenwirkungen sauber kapseln
Der häufigste Fehler: Die Datenbank ist idempotent, aber Side-Effects sind es nicht. Beispiele sind E-Mail-Versand, Webhooks oder das Triggern externer Provider. Hier hilft, Nebenwirkungen an den Zustand zu binden: Erst wenn die Operation final als completed markiert ist, wird ein nachgelagerter Prozess gestartet. Das kann synchron (nach Commit) oder asynchron (per Outbox) passieren.
Outbox-Pattern für Events und Integrationen
Wenn nach dem Speichern ein Event publiziert werden soll, ist das Outbox-Pattern ein bewährter Weg: Das Backend schreibt in derselben Datenbanktransaktion sowohl die fachlichen Daten als auch einen Outbox-Eintrag. Ein separater Publisher liest die Outbox und sendet das Event an Broker oder Webhook-Ziele. Damit sind Daten und Events konsistent, auch bei Crashes zwischen „DB-Commit“ und „Event gesendet“.
Timeouts und „in_progress“ sinnvoll behandeln
Ein Client kann ein Timeout sehen, obwohl das Backend weiterarbeitet. Wird dann erneut gesendet, sollte die API nicht blind neu starten. Für lange Vorgänge sind zwei Designs gängig:
- Synchron: Beim Wiederholen liefert die API den bereits gespeicherten Erfolg oder wartet bis completion (mit harter Obergrenze).
- Asynchron: Die API liefert eine Operation-ID, der Client fragt den Status ab, bis das Ergebnis verfügbar ist.
Wichtig ist, dass „in_progress“ Einträge eine definierte Ablaufzeit haben und sich sicher wieder aufnehmen lassen (z. B. durch Jobs, die hängen gebliebene Operationen erkennen und sauber abschließen oder als failed markieren).
Wie Clients Idempotenz korrekt nutzen
Key-Erzeugung und Wiederverwendung
Clients sollten den Key stabil erzeugen und beim Retry wiederverwenden. Ein zufälliger Wert pro Versuch zerstört die Wirkung. In mobilen Apps sollte der Key auch App-Restarts überleben können, wenn der Nutzer dieselbe Aktion erneut ausführt (z. B. lokale Persistenz bis zur Bestätigung).
Konflikte erkennbar machen
Wenn ein Key mit anderem Payload erneut auftaucht, ist das ein Signal für einen Client-Bug oder einen Integrationsfehler. Die API sollte dann nicht „raten“, sondern klar reagieren. Eine konfliktbasierte Antwort ist besser als stillschweigende Inkonsistenz.
Pragmatische Entscheidungshilfe: Wo Idempotenz wirklich nötig ist
Operationen mit Geld, Kontingenten und externen Effekten
Besonders kritisch sind Aktionen, die irreversible Nebenwirkungen haben: Zahlungen, Provisionierung, Versand, Kontingente, Tickets, Rechnungen. Hier sollte Idempotenz als Standard gelten. Für rein interne, leicht rückgängig zu machende Änderungen kann der Aufwand im Verhältnis geringer sein.
Read-Model vs. Write-Model
GET-Endpunkte profitieren indirekt: Wenn write-Requests robust sind, sinkt der Bedarf an „Schadensbegrenzung“ durch aufwendige Korrekturen. Der Fokus liegt dennoch klar auf Create/Update-Operationen und Integrationspunkten.
Konkreter Umsetzungsplan für bestehende APIs
Schrittfolge, die in laufenden Systemen funktioniert
- Schreibende Endpunkte inventarisieren: Welche Requests erzeugen Ressourcen oder triggern externe Effekte?
- Pro Endpunkt festlegen, was „derselbe Request“ bedeutet (Scope + Fingerprint-Regeln).
- Idempotency-Speicher einführen: Tabelle mit Unique Constraint und definierter TTL.
- Response-Cache definieren: Welche Daten werden bei Wiederholung exakt zurückgegeben?
- Nebenwirkungen über Outbox oder klaren Post-Commit-Hook ausführen; Duplikate im Versandpfad abfangen.
- Monitoring ergänzen: Zähler für „duplicate hit“, „conflict“, „in_progress timeout“; Logs mit Key+Scope.
Tests, die Duplikate realistisch simulieren
Parallelität und Retries gezielt provozieren
Unit-Tests reichen selten aus, weil Idempotenz oft an Transaktionsgrenzen hängt. Sinnvoll sind Integrations- und Concurrency-Tests:
- Zwei parallele Requests mit gleichem Key: Erwartung ist genau eine fachliche Ausführung.
- Retry nach Timeout: Der zweite Request muss das Ergebnis wiederfinden.
- Key-Reuse mit anderem Payload: Erwartung ist ein klarer Konflikt.
Für HTTP-basierte Systeme sollten Tests außerdem Statuscodes und Response-Bodies vergleichen, nicht nur den Datenbankzustand.
Abgrenzung zu Authentifizierung und Traffic-Schutz
Idempotenz löst ein anderes Problem als Auth und Limits
Idempotenz reduziert doppelte Effekte, verhindert aber keine unberechtigten Aufrufe und bremst auch keinen Missbrauch. In der Praxis greift das ineinander: Ein sauberer Auth-Layer, kontrollierte Retries und Schutzmechanismen gegen übermäßige Last ergänzen sich. Für angrenzende Themen passen z. B. JWT-Auth im Backend und Rate Limiting für APIs als Vertiefung, weil dort andere Failure-Modes adressiert werden.
Release-Strategien für neue Idempotenz-Regeln
Wenn Clients erst angepasst werden müssen, ist eine schrittweise Einführung sinnvoll: Zunächst den Header akzeptieren und beobachten, dann für kritische Endpunkte verpflichtend machen. Für kontrollierte Rollouts helfen Mechanismen wie Feature Flags im Produktivbetrieb, um neue Validierungen graduell zu aktivieren.
Kurzer Vergleich: Drei Wege zu doppel-sicheren Create-Requests
| Ansatz | Stärken | Grenzen |
|---|---|---|
| Unique Constraint auf fachlicher Referenz (z. B. client_order_id) | Sehr robust, wenig Logik, DB garantiert Eindeutigkeit | Funktioniert nur, wenn eine passende Referenz existiert und sauber modelliert ist |
| Idempotency-Key + gespeicherte Response | Flexible Abdeckung vieler Endpunkte, sauberes Retry-Verhalten | Zusätzlicher Storage/TTL-Management, Konfliktlogik nötig |
| Outbox-Pattern für Nebenwirkungen | Verhindert doppelte externe Effekte, verbessert Konsistenz | Mehr Komponenten (Publisher/Worker), Monitoring erforderlich |
In der Praxis ist die Kombination aus Unique Constraints (für Datenintegrität) und Idempotency-Key (für Client-Retries) besonders effektiv. Das Outbox-Pattern stabilisiert zusätzlich alles, was außerhalb der eigenen Datenbank passiert.
Weitere Beiträge rund um Engineering-Themen stehen im Bereich Software & Entwicklung.
