Idempotency Key: Ödeme ve Sipariş API’lerinde Çift İşlemi Bitiren Basit Kural
Tekrar eden isteklerde aynı sonucu garanti ederek ödeme/sipariş akışlarında çift kayıt ve çift çekimi engelleyin.
Modern API’lerde en can yakıcı hatalardan biri: kullanıcı “Öde”ye iki kez basar, mobil ağ kopar ve istek yeniden gönderilir, ya da reverse proxy timeout sonrası client retry yapar… Sonuç: çift sipariş veya daha kötüsü çift tahsilat.
Bu sorunu “retry yapma” diyerek çözemeyiz; retry, dağıtık sistemlerde gereklidir. Çözüm: idempotency.
Idempotency nedir?
Bir isteğin kaç kez gönderilirse gönderilsin aynı etkiyi üretmesi.
Örnek: POST /payments normalde idempotent değildir (her POST yeni kayıt yaratır). Idempotency Key ekleyerek idempotent hale getirirsiniz.
Ne zaman gerekli?
- Ödeme başlatma, tahsilat, iade gibi para hareketi olan uçlar
- Sipariş oluşturma, kargo oluşturma gibi tekil kaynak üreten uçlar
- Mobil istemciler ve kötü bağlantı koşulları
- Otomatik retry yapan HTTP client’lar (örn. timeout sonrası)
Tasarım: İstek başına bir anahtar
İstemci her “işlem niyeti” için benzersiz bir anahtar üretir ve header olarak gönderir:
POST /payments
Idempotency-Key: 8f1c7b3e-7c47-4d0b-9f5c-6d7b4b2d3a1e
Content-Type: application/json
{"orderId":"123","amount":199.90,"currency":"TRY"}
Sunucu tarafında kural basit:
- Bu anahtarla daha önce işlem yapıldı mı?
- Yapıldıysa aynı sonucu dön (aynı response body + status)
- Yapılmadıysa işlemi gerçekleştir, sonucu sakla
Veritabanı şeması (örnek)
Minimal bir tablo iş görür:
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
request_hash TEXT NOT NULL,
response_status INT,
response_body JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX ON idempotency_keys (expires_at);
Neden request_hash?
Aynı Idempotency-Key ile farklı payload gelirse bu genelde bug veya saldırı belirtisidir. Bu durumda 409 Conflict dönmek mantıklıdır.
Akış (uygulama mantığı)
Pseudocode:
key = header("Idempotency-Key")
assert key not null
hash = sha256(request.body)
BEGIN;
row = SELECT * FROM idempotency_keys WHERE key = :key FOR UPDATE;
IF row exists:
IF row.request_hash != hash: ROLLBACK; return 409
IF row.response_body is not null: COMMIT; return cached response
ELSE: COMMIT; return 202 (hala işleniyor) // opsiyonel
ELSE:
INSERT idempotency_keys(key, request_hash, expires_at) VALUES(...)
COMMIT;
// Asıl iş: ödeme sağlayıcısını çağır, siparişi oluştur vb.
result = do_business()
UPDATE idempotency_keys SET response_status=?, response_body=? WHERE key=?;
return result
“Hala işleniyor” senaryosu
Aynı key ile iki istek aynı anda gelirse:
- İlki kaydı açar
- İkincisi kaydı görür ama response henüz yazılmamıştır
Bu durumda iki yaklaşım var:
- 202 Accepted dönüp client’a “biraz sonra aynı key ile tekrar dene” demek
- Kilidi daha uzun tutup aynı işlemi bekletmek (genelde önerilmez; dış servis çağrıları uzun sürebilir)
Süre (TTL) nasıl seçilir?
Idempotency kayıtlarını sonsuza kadar tutmak zorunda değilsiniz.
- Ödeme gibi kritik işlemlerde: 24–72 saat
- Sipariş oluşturma: 1–24 saat
TTL, retry penceresini kapsamalı. Cleanup için expires_at < now() kayıtlarını periyodik silin.
İnce noktalar
- Anahtarı kim üretir? Genelde client (mobil/web). Backend de üretebilir ama client retry’larında aynı key’i tekrar gönderebilmek önemli.
- Sadece ödeme değil: “kupon kullan”, “stok düş”, “kayıt oluştur” gibi yan etkili her işlem adaydır.
- Response cache etmek şart mı? Evet. Aynı key’e aynı sonucu dönmek idempotency’nin özüdür.
- Log/izleme: Anahtarı log’lamak, “bu işlem kaç kez denendi?” sorusuna net cevap verir.
Küçük bir örnek: Sipariş oluşturma
- Client:
Idempotency-KeyilePOST /orders - Server: İlk çağrıda
order_id=789üretir ve response’u saklar - Client timeout yaşayıp tekrar gönderirse: Server aynı response’u döner, yeni sipariş açmaz
Sonuç
Idempotency Key, dağıtık sistemlerin kaçınılmazı olan retry’ları güvenli hale getirir. Özellikle ödeme ve sipariş akışlarında, “nadiren oluyor ama çok pahalı” türü hataları kökten keser.
İstersen bir sonraki adım olarak kendi stack’ine göre (Node.js/Go/.NET) kısa bir örnek implementasyon da paylaşabilirim.