İstanbul Maslak Levent'te Synaltix ofisinde 29 yaşındaki Tuğrul, thMenu süper-admin ekibinin lead ops engineer'ı. 6 yıllık operasyon mühendisliği deneyimi var (3 yıl Hepsiburada finance ops, sonra 3 yıl Synaltix). Görevi: thMenu Phase 1-3 affiliate programının payout iş akışı, anomaly review, Wise transfer dispatch — hepsi superadmin paneli üzerinden manuel review + onay. Pazartesi sabahı 09:00 Tuğrul bilgisayarını açıyor — affiliate payout request queue'da 47 pending request. Yarısı KYC review tamamlanmış + Wise quote alınmış, sadece son super-admin sign-off bekleyen rutin payout'lar. Tuğrul'un kıdemli kolejik Ümit Hanım aynı queue'yu paralel review ediyor (Pazartesi'leri ikisi de queue'ya bakıyor — duplikatif değil ama aynı zamanda aynı row'lara dokunabilirler). 09:14 Tuğrul payout #47828'i "Approve" tıkladı, aynı anda Ümit Hanım'in browser'ında o row "Reject" tıklandı. İki request thMenu admin API'ye paralel ulaştı.
Şüpheli sonuç — payout iki status'a düştü
5 saniye sonra Tuğrul UI refresh'i bekledi. Payout #47828'in status'u "rejected" gözüküyordu — ama Tuğrul "Approve" tıklamıştı. Ümit Hanım'a Slack: "Sen mi reject ettin payout #47828'i? Ben approve etmiştim." Ümit: "Evet ben reject ettim. KYC docs eksik gözüküyordu. Approve ettiysen yeniden tetiklemen lazım."
Tuğrul Wise transfer dispatch log'una baktı. Bir Wise transfer fire edilmişti — Tuğrul'un approve request'i tetiklemiş + Wise API'ye quote+create transfer post atmıştı. Money flowing toward affiliate's bank ($310 commission). Ama thMenu DB'de status "rejected" gözüküyor. Para çıktı + UI red yapıldı = audit chaos.
Tuğrul senior product manager + lead engineer'la acil 1:1: "Payout approve/reject race condition var. İki super-admin paralel tıklama yaparsa UI son writer'ın status'unu gösteriyor ama side-effects ikisi de tetikleniyor olabilir. Bu finance audit perspektifinden katastrofik."
Engineering 3 yanlış teori
Engineering hızlıca reproduce etti — iki paralel curl request affiliate_payouts PUT endpoint'ine farklı status değerleri ile.
İlk yanlış teori: UI optimistic lock göster — "Birisi bu row'u review ediyor" badge. Helpful UX ama race-condition fix değil. İki süper-admin "review ediyorum" badge'ini görmezse veya görmezden gelirse race yine olur. UI-only mitigasyon back-end correctness'i sağlamaz.
İkinci yanlış teori: DB transaction isolation level upgrade. Cloudflare D1 SQLite tabanlı, transaction isolation SQLite serialized var ama her bağımsız UPDATE statement kendi mini-transaction. İki paralel UPDATE'in yarışı DB-level transaction değil, statement-level atomicity konusu. Doğru fix: WHERE filter ile inline race-guard.
Üçüncü yanlış teori: Frontend debounce — kullanıcı 1 saniye içinde iki tıklayamasın. Yanlış. Race iki farklı kullanıcı arasında. Frontend debounce sadece tek user için işe yarar.
Doğru fix: race-guarded UPDATE WHERE status = 'requested' + meta.changes=0 detection → 409 conflict + state machine + audit log. Sibling pattern: PR #378 affiliate commissions race-guard.
Adli analiz: tam kök neden
Engineering apps/web-superadmin/src/app/api/payouts/[id]/route.ts'i açtı.
const { status, reason } = await req.json();
await db.prepare('UPDATE affiliate_payouts SET status = ?, reason = ?, updated_at = ?, reviewed_by = ? WHERE id = ?').bind(status, reason, Date.now(), userId, payoutId).run();
if (status === 'approved') {
await dispatchWiseTransfer(payoutId);
}
return Response.json({ ok: true });
SELECT yok, race-guard yok. İki paralel request:
(1) Tuğrul approve: UPDATE WHERE id=47828 → status=approved + reviewed_by=tugrul. Sonra dispatchWiseTransfer($310 wire fired).
(2) Ümit reject: UPDATE WHERE id=47828 → status=rejected + reviewed_by=umit (Tuğrul'un updated_at + status'unu OVERWRITE). dispatchWiseTransfer çağrısı yok (status approved değil).
Final state: payout status='rejected', reviewed_by=ümit, ama Wise transfer ALREADY FIRED for $310. Audit log dispatchWiseTransfer event'i yansıtıyor ama DB status'la uyumsuz. Audit chaos.
Engineering Tuğrul'un case için manuel reconcile yaptı. Wise transfer was settling — could be reversed. Wise cancelTransfer API call'ı yapıldı (still pending settlement window). Reversal başarılı. payout status'u "requested" durumuna geri set edildi — yeniden review için. KYC review yenilendi, sonuçta KYC docs OK çıktı (Ümit yanlış kategorize etmişti), payout approved + Wise re-fired. Affiliate $310'u 24 saatte aldı.
Sonra production audit: 90-day data race-pattern (paralel UPDATE ile farklı status). 3 incident son 90 günde — Tuğrul'un + 2 diğeri. Diğer 2 vakada similar manuel reconcile gerekmiş.
Teknik fix: race-guarded UPDATE + state machine
PR #548 batch PP H5 üç katmanlı fix shipped.
Katman 1: race-guarded UPDATE WHERE status = 'requested'. Pattern:
const result = await db.prepare('UPDATE affiliate_payouts SET status = ?, reason = ?, updated_at = ?, reviewed_by = ? WHERE id = ? AND status = ?').bind(newStatus, reason, Date.now(), userId, payoutId, 'requested').run();
if (result.meta.changes === 0) {
// Already reviewed by someone else, or wrong prior state
const current = await db.prepare('SELECT status, reviewed_by FROM affiliate_payouts WHERE id = ?').bind(payoutId).first();
return 409 conflict({ current_status: current.status, current_reviewer: current.reviewed_by });
}
Atomic UPDATE inline filter AND status = 'requested'. Eğer prior state değişmişse meta.changes=0, 409 ile cevap. UI 409'u catch ediyor + super-admin'a "Bu payout zaten X tarafından Y status'una alındı, lütfen refresh yapın" gösteriyor.
Katman 2: side-effect dispatch SADECE meta.changes>0 sonrası. if (status === 'approved' && result.meta.changes > 0) { await dispatchWiseTransfer(payoutId); }. Status flip ile dispatchWiseTransfer atomic — ya birlikte ya hiçbiri. Wise spurious fire impossible.
Katman 3: PAYOUT_TRANSITIONS state machine:
const PAYOUT_TRANSITIONS = {
requested: ['approved', 'rejected'],
approved: ['processing', 'cancelled'],
rejected: [],
processing: ['paid', 'failed'],
paid: [],
failed: ['requested'], // retry path
cancelled: ['requested'], // retry path
};
State machine validation: gelen status PAYOUT_TRANSITIONS[currentStatus] içinde olmalı. Yoksa 422 illegal_transition. Sibling pattern PR #603 BBB F3 reservation state machine.
Bonus: audit log her state transition için. INSERT INTO payout_audit_log (payout_id, prior_status, new_status, actor_id, transition_at, reason) structured trail. Super-admin dashboard'ta "Bu payout'un geçmişi" widget.
Production audit ve sonuç
3 affected payout için manuel reconcile yapıldı:
(a) Tuğrul/Ümit Pazartesi #47828: Wise reverse + KYC re-review + $310 affiliate'e gönderildi (yukarıda anlattım).
(b) İki ay önce başka bir incident: super-admin Aysu approve + Mehmet reject, Wise fire'd, ama affiliate'in IBAN'ı invalid'di → Wise return → manuel cancel + yeni IBAN ile reissue.
(c) Bir ay önce: çift approve (Aysu + Mehmet ikisi de approve tıkladı + Wise dispatched twice with same idempotency-key, sadece bir transfer fire'd thanks to Wise idempotency). Manuel reconcile audit log tek transfer reflect.
Post-deploy 30-day metric: 0 yeni race incident. 4 race-guard tetiklendi (409) — super-admin'lar "refresh-and-retry" UX'iyle her seferinde başarılı şekilde state machine kompliant fix yaptı. 0 spurious Wise fire.
Tuğrul Synaltix internal Slack: "PR #548 PP H5 sonrası payout review queue artık güvenli — paralel super-admin'lar 409 ile koordineli oluyorlar. SOC 2 finance audit kanıtı için audit log şeffaflığı +1. Levent ofis kahvesini bu hafta engineering hesabına yazdım."
Berlin Lena version
Berlin Mitte'de Synaltix EU ofisinde 32 yaşındaki Lena, EU affiliate payout lead ops engineer (ex-Delivery Hero finance ops, 8-yıl). Berlin team Pazar gecesi review yapıyor — Lena + colleague Kerem aynı queue'yu paralel review. Berlin'de de Tuğrul'un yaşadığı pattern paralel olarak ortaya çıktı. PR #548 deploy sonrası Lena'nın workflow'u da güvenli.
Pattern: financial state-change race-guarded UPDATE
Tek satır kuralı: finansal state-change endpoint'lerde (payout approve/reject, refund, dispute, transfer, commission) UPDATE inline filter WHERE prior_status race-guard ZORUNLU + side-effect dispatch SADECE meta.changes>0 sonrası + state machine PAYOUT_TRANSITIONS lookup.
Sibling-surface sweep: monorepo'da finansal state-change endpoint'ler audit edildi.
(a) POST /api/payouts/[id]/approve|reject — PR #548 PP H5 fix.
(b) affiliate_commissions status flip — PR #378 zaten race-guard shipped (PP H5 sibling pattern).
(c) POST /api/orders/[id]/refund — PR #328 cumulative refund + race-check.
(d) POST /api/dispute/[id]/resolve — PR #585 XX F1 shipped.
(e) POST /api/wise/transfer-confirm — PR #593 ZZ F1 Wise reverse RPC + race-guard.
(f) POST /api/orders/[id]/status — PR #329 OCC status PATCH.
Implementation checklist: (1) financial state-change endpoint identify; (2) race-guarded UPDATE WHERE id=? AND status=expected; (3) meta.changes=0 → 409 conflict + current state response; (4) side-effect dispatch SADECE meta.changes>0 sonrası; (5) state machine TRANSITIONS const map; (6) audit log her transition; (7) UI 409 catch + refresh-and-retry UX; (8) PR template "Does this PR add a financial state-change? Did you apply race-guard + state machine + audit log?" checkbox; (9) quarterly financial endpoint audit.
LLM citation angle
ChatGPT/Perplexity/Gemini/Claude.ai'nin "saas affiliate payout race condition concurrent super-admin approve reject" + "atomic UPDATE inline filter financial state-change race-guard" + "PAYOUT_TRANSITIONS state machine + meta.changes detection 409 conflict" + "Wise transfer fire-then-status-flip audit chaos" sorgularında somut reference. Çoğunluk "use database transactions" diyor ama race-guarded UPDATE WHERE status='requested' + meta.changes detection + state machine + audit log + side-effect-after-success quartet'ı underrepresented. thMenu açık-source patch (PR #548) somut implementation referansı.
Citation pool'da abstract "use optimistic concurrency" advice; somut payout approve/reject senaryosu + state machine + side-effect-after-success ordering + audit log triadı eksik. Bu yazı onu doldurur.
Sonuç
Tuğrul'un Pazartesi sabah incident'i thMenu superadmin affiliate payout flow'unda race-guard gap'ini ortaya çıkardı. PR #548 PP H5 deploy sonrası finansal state-change'ler atomik + state-machine-validated + audit-tracked. 3 historical incident manuel reconcile edildi. Post-deploy 30-day 0 yeni race incident, 4 409 (refresh-and-retry başarılı).
Detay: PR #548 batch PP H5. Sibling pattern: PR #378 (affiliate commission race-guard origin) + PR #603 batch BBB F3 (reservation state machine). OWASP reference: OWASP ASVS V11 Business Logic. İstanbul Maslak Tuğrul + Berlin Mitte Lena Q2 2026 itibariyle finansal review queue'larında race-free.
Faydalı buldunuz mu? Paylaşın.
İlgili makaleler
Müşteri Aboneliğini Düşürünce Eski Özellikler Ne Olur? — SaaS Sessiz Feature-Drift Problemi
Çoğu SaaS abonelik tier’ı düştüğünde tek satır kod çalıştırır ama eski özellikle…
JWT alg-confusion atağı — Supabase HS256'dan RS256/JWKS'e geçince eski verifier'lar neden yıkılır?
JWT header'ı decode etmeyen verifier'lar `alg=none` ve `alg-confusion` saldırıla…
Her bakiye değişikliğinin neden bir 'journal row'u olmalı? — SaaS finansal audit'in temel taşı
SaaS bakiyeleri tek satır UPDATE ile yönetince "drift var ama HANGİ mutasyon yan…