E-ticaret ve finans (Fintech) dünyasının en klasik kabus senaryosunu hayal et: Kullanıcın metroda, internet bağlantısı gidip gelirken sepetindeki ürün için "2.500 TL Öde" butonuna bastı. İstek senin backend sunucuna ulaştı ve bankadan para çekildi; ancak tam o saniyede kullanıcının interneti koptuğu için "Ödeme Başarılı" cevabı telefonuna ulaşamadı.
Kullanıcı ekranda dönen yükleme ikonuna sinirlenip "Öde" butonuna üst üste iki kere daha bastı (ya da mobil uygulama zaman aşımına uğrayan isteği arka planda otomatik olarak tekrar gönderdi).
Sonuç? Kullanıcının telefonuna bankadan üst üste 3 SMS geldi: Aynı sipariş için kredi kartından 3 kere 2.500 TL çekildi!
1. "Frontend'de Butonu Disabled Yaparız Çözülür" Yanılgısı
Bu senaryoyu iş görüşmelerinde sorduğumda pek çok geliştiricinin ilk cevabı şu oluyor: "Çok basit, React tarafında butona tıklandığı anda disabled={isLoading} yaparız, bir daha basamaz."
Frontend'de butonu pasif yapmak kullanıcı deneyimi (UX) için şarttır ama backend güvenliği için asla yeterli değildir:
- Ağ Katmanı Tekrarları (Automatic Retries): Mobil uygulamalar, API Gateway'ler veya ödeme sağlayıcı webhook'ları, TCP bağlantısı koptuğunda cevabı alamadıkları için aynı HTTP isteğini otomatik olarak tekrar gönderir.
- İki Sekme veya Yarış Durumu (Race Condition): Kullanıcı sepeti iki farklı sekmede açmış olabilir veya aynı milisaniyede çift tıklama paketi ağa çıkmış olabilir.
- İki General Problemi (Two Generals' Problem): İstemci (telefon) ile sunucu arasındaki bağlantı yanıt sırasında koptuğunda, istemci paranın çekilip çekilmediğini bilemez; güvenli şekilde tekrar sorabilmesi gerekir.
İşte gerçek dünyada Stripe, iyzico, PayTR ve bankacılık sistemlerini bu felaketten koruyan mimari kavramın adı Idempotency (Etkisizlik) ve Idempotency Key'dir.
2. Idempotency ve Idempotency Key Tam Olarak Nedir?
Matematikte ve bilgisayar bilimlerinde bir işlemin Idempotent (Etkisiz) olması; o işlemi 1 kere çalıştırmakla 100 kere çalıştırmanın sistemde tamamen aynı sonucu üretmesi demektir.
GET /siparis/123veyaDELETE /siparis/123doğası gereği idempotent'tir.- Ancak
POST /odeme-yapdoğası gereği idempotent değildir; her çağrıldığında yeni bir tahsilat başlatır.
POST /odeme-yap isteğini güvenli hale getirmek için, istemci (frontend veya mobil uygulama) ödeme ekranı açıldığında o ödeme niyeti için benzersiz bir UUID (Idempotency-Key) üretir ve bunu HTTP başlığında (Header) gönderir:
POST /api/v1/payments/charge HTTP/1.1
Host: api.czay.dev
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
Content-Type: application/json
{
"orderId": "ord_8841",
"amount": 250000,
"currency": "TRY"
}Kullanıcı butona 3 kere de bassa, internet kopup uygulama isteği 5 kere tekrar da gönderse hepsinin içinde aynı Idempotency-Key yer alır!
3. Adım Adım Backend Kurulumu (Redis Atomic Lock + PostgreSQL)
Backend sunucun Idempotency-Key başlığına sahip bir ödeme isteği aldığında şu atomik adımları uygular:
export async function processPayment(req, res) {
const idempotencyKey = req.headers["idempotency-key"];
if (!idempotencyKey) {
return res.status(400).json({ error: "Idempotency-Key başlığı zorunludur." });
}
const redisKey = `idempotency:${req.user.id}:${idempotencyKey}`;
// 1. Atomik Kilit (SET NX: Sadece bu anahtar Redis'te YOKSA yaz ve 24 saat tut)
const lockAcquired = await redis.set(
redisKey,
JSON.stringify({ status: "PROCESSING" }),
"EX",
86400,
"NX"
);
// Eğer anahtar zaten varsa (Kullanıcı 2. veya 3. kez bastıysa):
if (!lockAcquired) {
const existingRecord = JSON.parse(await redis.get(redisKey));
// İlk istek şu an hala bankayla konuşuyorsa 409 Conflict dön:
if (existingRecord.status === "PROCESSING") {
return res.status(409).json({
error: "Ödemeniz şu anda işleniyor, lütfen bekleyin.",
});
}
// İlk istek zaten tamamlandıysa, bankaya HİÇ gitmeden ilk isteğin makbuzunu aynen dön!
return res.status(existingRecord.statusCode).json(existingRecord.body);
}
try {
// 2. İlk kez gelen geçerli istek: Sanal POS (iyzico / PayTR / Stripe) çekimini yap
const paymentResult = await paymentGateway.charge(req.body);
// 3. Başarılı sonucu Redis'e kaydet ki sonraki tekrarlar bu makbuzu alsın
await redis.set(
redisKey,
JSON.stringify({
status: "COMPLETED",
statusCode: 200,
body: paymentResult,
}),
"EX",
86400
);
return res.status(200).json(paymentResult);
} catch (error) {
// Bankaya ulaşılamadan ağ hatası olduysa kilidi sil ki kullanıcı tekrar deneyebilsin
await redis.del(redisKey);
throw error;
}
}Bilgi: Altın Kural: Redis Kilidini Veritabanı UNIQUE İndeksiyle Destekle!
Redis üzerindeki SET ... NX kilidi milisaniyeler içinde yarış durumlarını (race condition) yakalar; ancak tam kurumsal güvenlik için PostgreSQL'deki payments tablonda da idempotency_key sütununa mutlaka UNIQUE (Benzersizlik) kısıtı eklemelisin. Böylece Redis anlık olarak yeniden başlasa bile veritabanı ikinci kaydın yazılmasını fiziksel olarak reddeder.
İlgili Kısa Video & Ödeme Rehberlerimiz
İnteraktif Yol Haritası & Araç
System Design: Milyon Kullanıcıya Ölçeklenmek
Sunucuyu çoğaltmak yetmiyor: büyük bir günde sistemin neden çöktüğünü ve yükün veritabanına ulaşmadan nasıl kesildiğini aşama aşama anlatan System Design haritası.
SQL mi NoSQL mi?
Altı soruya cevap ver, projene hangi tür veritabanının uyduğunu ve nedenini söyleyeyim.
Bunu da oku
- CZTRDokku Nedir? Ayda 200$ Bulut Faturasını 5$ Sunucuya Düşüren Açık Kaynak PaaS Mimarisi
Dokku Nedir? Ayda 200$ Bulut Faturasını 5$ Sunucuya Düşüren Açık Kaynak PaaS Mimarisi
25 Ekim 2026 · 5 dk okuma - CZTROrtalık Çok Karışık: Backend Geliştirmeye Başlarken Hangi Dil ve Framework Seçilmeli? (2026 Karar Rehberi)
Ortalık Çok Karışık: Backend Geliştirmeye Başlarken Hangi Dil ve Framework Seçilmeli? (2026 Karar Rehberi)
25 Ekim 2026 · 5 dk okuma