Sorunun Analizi
Ödeme sayfasında kullanıcı "Öde" butonuna tıkladığında, backend'e bir istek gönderiliyor ve ödeme işlemi başlıyordu. Ancak, internet bağlantısı yavaş olduğunda veya kullanıcı sabırsızlandığında butona arka arkaya (double click) tıklayabiliyordu.
Bu durumda, ilk istek henüz tamamlanmadan ikinci bir istek sunucuya ulaşıyor ve sunucu her iki isteği de geçerli kabul edip işliyordu. Sonuç olarak:
- Kullanıcının kartından iki kez para çekiliyordu.
- Veritabanında iki ayrı sipariş kaydı oluşuyordu.
- Stok sayımı bozuluyordu.
Bu klasik bir Race Condition (Yarış Durumu) problemidir.
Çözüm Yaklaşımı
Frontend tarafında State Machine ile UI kontrolü, Backend tarafında Idempotency Key ile işlemin benzersizliğinin garanti altına alınması.
1. Frontend: State Machine (Durum Makinesi)
Sadece isLoading değişkeni kullanmak yetersiz kalabilir. Bunun yerine, ödeme sürecini bir durum makinesi gibi düşündük: IDLE -> PROCESSING -> SUCCESS | ERROR. UI, sadece IDLE durumundayken tetiklenebilir.
2. Backend: Idempotency Key (Tekrarlanamazlık Anahtarı)
Frontend önlemleri (buton disable etmek vb.) kullanıcı tarafından aşılabilir (JS konsolundan). Bu yüzden asıl çözüm Backend'dedir.
Her ödeme isteği için Frontend benzersiz bir Idempotency Key (genellikle UUID) üretir ve Header'a ekler. Backend, bu anahtarı (örneğin Redis'te) kısa süreliğine saklar. Eğer aynı anahtarla ikinci bir istek gelirse, işlemi tekrar yapmaz, ilk işlemin sonucunu (veya "İşlem sürüyor" hatasını) döner.
Kod Uygulaması
// 1. Frontend (Vue Composable)
const usePayment = () => {
const status = ref<'IDLE' | 'PROCESSING' | 'SUCCESS' | 'ERROR'>('IDLE');
const pay = async (data) => {
if (status.value === 'PROCESSING') return; // Guard Clause
status.value = 'PROCESSING';
const idempotencyKey = crypto.randomUUID(); // Unique Key
try {
await api.post('/pay', data, {
headers: { 'Idempotency-Key': idempotencyKey }
});
status.value = 'SUCCESS';
} catch (e) {
status.value = 'ERROR';
}
};
return { pay, status };
};
// 2. Backend (Middleware / Logic)
async function handlePayment(req, res) {
const key = req.headers['idempotency-key'];
// Redis'te bu anahtar var mı?
const cachedResult = await redis.get(key);
if (cachedResult) {
return res.json(cachedResult); // Önceki sonucu dön
}
// İşlemi yap (Atomik olmalı)
const result = await paymentService.charge(req.body);
// Sonucu cache'le (örn: 24 saat)
await redis.set(key, result, 'EX', 86400);
return res.json(result);
}