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.

Distributed payment engine architecture diagram

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

  1. Werden Payment-Updates mit WHERE version = ? durchgeführt?
  2. Liest der Handler bei Version Conflict neu und prüft er terminalen Status?
  3. Ist die Rückgabe von Client Secret oder Redirect-URL bei terminalen Status auf Code-Ebene blockiert?
  4. Erwirbt der Webhook-Handler vor der Verarbeitung einen Lease?
  5. Ist die Lease-TTL länger als die P99-Webhooks-Verarbeitungszeit?
  6. Teilen Sync-Handler und Webhook-Handler dieselbe Finalize-Logik?

Was bleiben soll

  1. Webhook- und Sync-Pfad berühren denselben Datensatz gleichzeitig; optimistische Concurrency ist die Standardantwort auf dieses Rennen.
  2. Ein Version Token verhindert Lost Updates; ein Lease verhindert parallele Verarbeitung desselben Datensatzes.
  3. Ein Version Conflict ist kein Fehler — es ist ein Signal zum Neu-Lesen.
  4. 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

Nachster Teil der Serie

Aus derselben Serie

Paylaş