Playbook
Optimistische Concurrency unter Webhooks (Optimistische Concurrency Unter Webhooks)
Wenn Webhook und synchrone Antwort dieselbe Zahlung gleichzeitig berühren: Wie lösen Version Token und Lease das Rennen — und warum kann ein veralteter Read…
Verteilte Payment Engine (Distributed Payment Engine)
Teil 17 von 22
Serie zur verteilten Payment-Architektur — die Lücke zwischen Capture und Complete.
Im vorherigen Teil haben wir gesehen, warum Eventual Consistency unvermeidlich ist: Der PSP kann niemals an Ihrem Transaktionsprotokoll teilnehmen, Saga plus Abgleich ist die echte Antwort. Dieser Teil konzentriert sich auf den intensivsten Moment innerhalb dieses Inkonsistenzfensters — wenn Webhook und synchrone Antwort gleichzeitig denselben Payment-Datensatz berühren.
Der Checkout-Orchestrator sendet eine Charge-Anfrage; das Provider-Gateway erhält eine Antwort vom PSP. Gleichzeitig — manchmal Millisekunden früher, manchmal später — kommt ein Webhook für dieselbe Zahlung an. Beide Pfade können korrekte Information tragen; versuchen beide gleichzeitig zu schreiben, ist das Ergebnis entweder ein verlorenes Update oder schlimmer: ein veralteter Read, der dem Client ein Secret oder eine Redirect-URL für eine Zahlung zurückgibt, die bereits terminal ist.
Sync-Antwort ──► Payment #42 (version=3) ──► Captured
Webhook ──► Payment #42 (version=3) ──► Captured (nochmal?)
│
▼
Version Token + Lease
→ ein Gewinner schreibt
Optimistische Concurrency ist hier keine Performance-Optimierung — sie ist der Mechanismus, der verhindert, dass falsche Daten den Client bei terminaler Zahlung erreichen.
Wo die Begriffe zuerst auftauchen
📦 Version Token (Optimistic Lock)
Ein Zähler, der bei jedem Update steigt; ein Write gelingt nur, wenn die erwartete Version passt.
📦 Lease
Ein zeitlich begrenzter Anspruch, der einem Worker das Recht gibt, einen bestimmten Payment-Datensatz zu verarbeiten.
📦 Stale Read
Eine während der Verarbeitung gelesene Version, die beim Write nicht mehr gültig ist.
📦 Terminal Payment
Ein Endzustand ohne Rückweg: Captured, Failed oder Refunded.
Ein Lease sagt 'ich verarbeite diesen Datensatz'; ein Version Token sagt 'ich sehe noch diese Version'. Zusammen verhindern sie, dass Webhook-Pfad und synchroner Pfad sich gegenseitig überschreiben.
Zwei Pfade, ein Datensatz: wo das Rennen beginnt
Bei redirect-basierten Zahlungen liefert der synchrone Pfad meist Pending; das echte Ergebnis kommt per Webhook. Bei Kartenzahlungen können beide Pfade Captured oder Failed tragen — und fast gleichzeitig ankommen. Der Checkout-Orchestrator versucht, beide in dieselbe Payment-Zeile zu schreiben.
Der klassische Fehler: Beide Handler lesen den Datensatz, aktualisieren den Status und speichern. Last write wins; das Update dazwischen verschwindet still. Das gefährlichere Szenario: Der Orchestrator liest eine alte Version, bevor er terminal wird, und gibt dem Client ein Secret oder eine Redirect-URL zurück, die noch gültig wirkt — obwohl die Zahlung bereits abgeschlossen oder fehlgeschlagen ist.
T=0 Orchestrator: sendet Charge
T=1 Webhook kommt → schreibt Captured (version 2→3)
T=2 Sync-Antwort kommt → hatte Pending gelesen (version 1)
→ gibt redirectUrl an Client zurück (veraltet!)
T=3 Kunde folgt Redirect → Zahlung bereits Captured
Version Token: schreiben nur, wenn die Version passt
Jeder Payment-Datensatz trägt ein monoton steigendes version-Feld. Updates nutzen: UPDATE ... WHERE id = ? AND version = ?. Kein Treffer bedeutet null betroffene Zeilen — ein Signal, dass ein anderer Pfad zuerst war.
Webhook-Handler
READ payment (version=2, status=Processing)
→ status=Captured, version=3
UPDATE WHERE version=2 ✓ (1 row)
Sync-Handler (veralteter Read)
READ payment (version=2, status=Processing) ← Webhook noch nicht committed
→ status=Captured, version=3
UPDATE WHERE version=2 ✗ (0 rows — Webhook hat schon geschrieben)
→ neu lesen, terminalen Status sehen, kein Client Secret zurückgeben
Ein Version Token allein reicht nicht; Sie brauchen auch eine definierte Reaktion auf erkannten Stale Read: neu lesen, terminalen Status prüfen, dem Client nur den aktuellen Ausgang zurückgeben.
Lease: Webhook-Verarbeitungsrecht zeitlich begrenzt beanspruchen
Bevor der Datensatz berührt wird, erwirbt der Webhook-Handler einen kurzen Lease: 'Ich verarbeite Payment #42 für 30 Sekunden.' Solange der Lease gilt, kann kein anderer Worker denselben Datensatz im Webhook- oder Recovery-Flow verarbeiten.
Webhook kommt an
→ Lease erwerben (paymentId, ttl=30s)
→ Lease nicht verfügbar → defer / retry
→ Lease erworben → Update mit Version Token
→ Lease freigeben
Ein Lease verhindert, dass derselbe Webhook von zwei Workern gleichzeitig verarbeitet wird. Ein Version Token löst Kollisionen zwischen verschiedenen Pfaden (Sync vs Webhook). Sie beantworten unterschiedliche Probleme und müssen zusammen verwendet werden.
Client-Secret-Leckage bei terminalen Zahlungen
Das schwerwiegendste Stale-Read-Szenario ist die Rückgabe eines Client Secrets oder einer Redirect-URL, nachdem die Zahlung terminal ist. Pending + redirectUrl nach Captured führt zu einem unnötigen zweiten Charge-Versuch oder Kundenverwirrung.
Die Regel ist einfach: Niemals ein Client Secret, eine Redirect-URL oder ein Retry-Token für eine Zahlung im terminalen Status zurückgeben. Bei Version Conflict oder Stale-Read-Verdacht liest der Handler den Datensatz neu; sieht er terminalen Status, gibt er nur das Endergebnis zurück.
| Status | An Client zurückgegeben |
|---|---|
| Processing, Redirect nötig | redirectUrl (gültig) |
| Captured (terminal) | Erfolgsergebnis, kein Secret |
| Failed (terminal) | Fehlerergebnis, kein Secret |
| Version Conflict → neu lesen → Captured | Erfolgsergebnis, kein Secret |
Häufig verwechselte Unterschiede
❌ Pessimistic Locking ist immer sicherer
✓ Kurzer Lease plus Version Token löst das Rennen bei erhaltenem Durchsatz
❌ Version Conflict = Exception werfen
✓ Version Conflict = anderer Pfad hat gewonnen; neu lesen und an aktuellen Status anpassen
❌ Lease und Version Token machen dasselbe
✓ Lease blockiert parallele Verarbeitung desselben Datensatzes; Version Token blockiert Lost Updates
Lease vs Version Token
| Kriterium | Lease | Version Token |
|---|---|---|
| Verhindert | Parallele Verarbeitung desselben Datensatzes | Lost Update |
| Dauer | Durch TTL begrenzt | Persistent, steigt bei jedem Write |
| Bei Konflikt | Warten / defer | Neu lesen / retry |
Checkliste für optimistische Concurrency
- Werden Payment-Updates mit
WHERE version = ?durchgeführt? - Liest der Handler bei Version Conflict neu und prüft er terminalen Status?
- Ist die Rückgabe von Client Secret oder Redirect-URL bei terminalen Status auf Code-Ebene blockiert?
- Erwirbt der Webhook-Handler vor der Verarbeitung einen Lease?
- Ist die Lease-TTL länger als die P99-Webhooks-Verarbeitungszeit?
- Teilen Sync-Handler und Webhook-Handler dieselbe Finalize-Logik?
Was bleiben soll
- Webhook- und Sync-Pfad berühren denselben Datensatz gleichzeitig; optimistische Concurrency ist die Standardantwort auf dieses Rennen.
- Ein Version Token verhindert Lost Updates; ein Lease verhindert parallele Verarbeitung desselben Datensatzes.
- Ein Version Conflict ist kein Fehler — es ist ein Signal zum Neu-Lesen.
- Ein Client Secret aus veraltetem Read bei terminaler Zahlung ist ein leiser Sicherheits- und UX-Fehler.
Sie brauchen kein Pessimistic Locking, um das Rennen zu lösen — Sie brauchen eine disziplinierte Kombination aus Version Token und Lease, die verhindert, dass veraltete Reads den Client erreichen.
Der nächste Teil geht zur Observability: Korrelation über Payment-ID, Schritt-für-Schritt-Event-Log und Metriken für deferred Finalize.
FAQ
Häufige Fragen
Was ist Version Token (Optimistic Lock)?
Ein Zähler, der bei jedem Update steigt; ein Write gelingt nur, wenn die erwartete Version passt.
Was ist Lease?
Ein zeitlich begrenzter Anspruch, der einem Worker das Recht gibt, einen bestimmten Payment-Datensatz zu verarbeiten.
Stimmt es, dass „Pessimistic Locking ist immer sicherer“?
Kurzer Lease plus Version Token löst das Rennen bei erhaltenem Durchsatz
Was legt dieser Teil fest?
Optimistische Concurrency ist hier keine Performance-Optimierung — sie ist der Mechanismus, der verhindert, dass falsche Daten den Client bei terminaler Zahlung erreichen. Webhook- und Sync-Pfad berühren denselben Datensatz gleichzeitig; optimistische Concurrency ist die Standardantwort auf dieses Rennen. Im vorherigen Teil haben wir gesehen, warum Eventual Consistency unvermeidlich ist: Der PSP kann niemals an Ihrem Transaktionsprotokoll teilnehmen, Saga plus Abgleich ist die echte Antwort. Dieser Teil konzentriert sich auf den intensivsten Moment innerhalb dieses Inkonsistenzfensters — wenn Webhook und synchrone Antwort gleichzeitig denselben Payment-Datensatz berühren.
Gelernte Engineering-Prinzipien
- Ein Version Token verhindert Lost Updates; ein Conflict ist ein Signal zum Neu-Lesen.
- Lease und Version Token lösen unterschiedliche Rennen und müssen zusammen verwendet werden.
- Niemals ein Client Secret aus veraltetem Read bei terminaler Zahlung zurückgeben.
Weiterlesen
Weiterlesen
Nachster Teil der Serie
Zahlungs-Observability und Korrelation
Wie korreliert man jedes Log, jede Metrik und jeden Trace über die Payment-ID — und warum Step-Event-Log plus Deferred-Finalize-Metriken den Betrieb retten.…
Nachster Teil der Serie
Warum Eventual Consistency verteilten Transaktionen überlegen ist
Eine 2PC über PSP, Bestellung und Finanzwesen aufzubauen ist eine Falle. Saga plus Abgleich ist die eigentliche Antwort, auf die dieser achtteilige Bogen…
Aus derselben Serie
Zahlungs-Recovery-Pipeline und Runbooks
Automatisierung zuerst: Abgleich-Worker und Recovery-Pipeline. Wenn Uniqueness-Wände Replay blockieren, übernehmen evidenzbasierte menschliche Runbooks.