Idempotency Key Nedir? Ödeme ve Sipariş API'lerinde Çift İşlemi Önlemek
Idempotency key, istemcinin ürettiği benzersiz bir anahtardır. Sunucu aynı anahtarla gelen ikinci isteği yeni işlem saymaz; ilk sonucun aynısını döner. Ödeme, sipariş ve iade uçlarında çift kaydı bu anahtar keser.
POST /payments doğası gereği idempotent değildir. Aynı gövde iki kez gelince iki çekim doğabilir. Kullanıcı butona iki kez basar, tarayıcı timeout alıp yeniden dener, mobil uygulama ağ kesilince retry atar. Anahtar yoksa üçü de ayrı işlemdir. Anahtar varsa üçü de aynı cevaptır. Ödeme geçidinde bu riski ayrı yazıda transaction statüsüyle kapatmıştım. Burada o kuralı API sözleşmesine taşıyorum.
Idempotency nedir?
Idempotency, bir işlemi bir kez yapmakla aynı işlemi N kez yapmak arasında sonucu değiştirmemektir. HTTP'de GET, PUT ve DELETE çoğu zaman bu tanıma yakındır. POST değildir. POST her çağrıda yeni kaynak üretir diye tasarlanır. Para hareketi, stok düşümü ve sipariş oluşturma bu yüzden POST'ta anahtarsız bırakılmaz.
- Aynı anahtar + aynı gövde → ilk cevabı tekrar dön.
- Aynı anahtar + farklı gövde → 409 Conflict. Sessizce ikinci işlemi yapma.
- Farklı anahtar → yeni işlem.
Anahtarı kim üretir?
Anahtarı istemci üretir. Sunucu üretirse, bağlantı kopup istemci yeniden denediğinde yeni anahtar gelir ve çift işlem yeniden açılır. Üretim anı buton tıklaması değildir. Ödeme niyeti oluştuğu andır: ödeme ekranı açılırken veya sipariş sepete kilitlenirken.
// Mobil / SPA: ekran açılırken bir kez üret, basışta yeniden üretme
var idempotencyKey = Guid.NewGuid().ToString("N");
Http.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);
UUID v4 yeter. Sipariş numarası da olur; yeter ki aynı niyet için sabit kalsın. Kullanıcı ekranı kapatıp yeni niyet başlatırsa yeni anahtar üretilir. Eski anahtarı yeniden kullanmak, eski cevabı getirir; yeni çekim yapmaz.
.NET'te minimal uç
[HttpPost("payments")]
public async Task<IActionResult> CreatePayment(
[FromHeader(Name = "Idempotency-Key")] string? key,
[FromBody] PaymentRequest request,
CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(key) || key.Length > 64)
return BadRequest(new { error = "Idempotency-Key zorunlu (max 64)." });
var bodyHash = HashBody(request);
var existing = await _store.FindAsync(key, ct);
if (existing is not null)
{
if (!FixedTimeEquals(existing.BodyHash, bodyHash))
return Conflict(new { error = "Aynı anahtar, farklı gövde." });
Response.Headers["Idempotent-Replayed"] = "true";
return StatusCode(existing.StatusCode, existing.ResponseBody);
}
// reserved: aynı anda iki worker aynı anahtarı almasın
var reserved = await _store.TryReserveAsync(key, bodyHash, ct);
if (!reserved)
{
existing = await _store.WaitUntilReadyAsync(key, TimeSpan.FromSeconds(15), ct);
return StatusCode(existing!.StatusCode, existing.ResponseBody);
}
var result = await _payments.ChargeAsync(request, ct);
await _store.CompleteAsync(key, bodyHash, 201, result, ct);
return Created($"/payments/{result.Id}", result);
}
Kritik sıra şudur: önce anahtarı kilitle, sonra bankaya git, sonra cevabı yaz. Bankaya gidip sonra anahtar yazarsanız, iki eşzamanlı istek ikisi de bankaya gider. Kilidi Redis SET key NX EX veya SQL'de unique index + insert ile kurun. Unique index, yarışta ikinci insert'i düşürür; uygulama onu "bekle ve ilk cevabı dön" diye okur.
Nerede saklanır?
- Tek instance ve düşük trafik: bellek yetmez. Process ölünce anahtar gider, retry çift çekim olur.
- Çok instance: Redis. Anahtar, gövde hash'i, HTTP status, cevap gövdesi, TTL.
- Mali iz: SQL tablosu. Redis hızlı kapı, SQL kalıcı kayıt. İade ve mutabakat bu tablodan yürür.
CREATE TABLE idempotency_keys (
idem_key varchar(64) PRIMARY KEY,
body_hash char(64) NOT NULL,
status_code int NULL,
response_body jsonb NULL,
state smallint NOT NULL, -- 1=reserved 2=completed
created_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz NULL
);
TTL şarttır. Stripe benzeri API'lerde anahtar genellikle 24 saat yaşar. Süresi dolmuş anahtarla gelen istek yeni işlemdir. Ödeme niyeti o süreden uzun yaşayacaksa TTL'yi iş kuralına yazın; sessizce varsaymayın.
Production'da kırılan yerler
Timeout sonrası istemci retry atar, sunucu aslında SUCCESS yazmıştır. Anahtar varsa ikinci istek ilk SUCCESS cevabını döner; kullanıcıya "zaten alındı" gösterilir. Anahtar yoksa ikinci çekim gider. Bu yüzden ödeme ekranında "Tekrar dene" butonu yeni anahtar üretmemeli; aynı niyetin anahtarını taşımalı.
Webhook ile karıştırmayın. Idempotency key istemciden API'ye giden istek içindir. Bankadan size gelen bildirim için ayrı bir olay kimliği gerekir; onu webhook yazısında kuruyoruz. SAGA telafisinde de aynı anahtar kuralı geçerlidir: telafi iki kez gelirse stok iki kez artmamalı. Dağıtık geri alma tarafını SAGA yazısında anlatmıştım.
Gövde hash'i zorunludur. Aynı anahtarla tutarı 100'den 1000'e çeviren istemci, sessizce ilk cevabı almamalıdır. 409 dönün. Logda anahtarı yazın, kart numarasını yazmayın. KVKK ve PCI tarafı KVKK kontrol listesinde.
Ne zaman zorunlu?
- Ödeme başlatma, tahsilat, iade, iptal
- Sipariş oluşturma, stok rezervasyonu
- Cüzdan yükleme, puan düşme, kontör yükleme
- Dış servise "bir kez" denmesi gereken her POST
Okuma uçlarına koymayın. Rapor GET'ine idempotency key eklemek maliyeti artırır, fayda vermez. Anahtarı API anahtarı veya kullanıcı kimliği sanmayın. Kimlik kim olduğunu söyler; idempotency key hangi niyet olduğunu söyler.
Kısa cevaplar
Idempotency key nedir?
İstemcinin ürettiği, sunucunun aynı işlemin tekrarını tanımasını sağlayan benzersiz anahtardır. Aynı anahtarla gelen tekrar, yeni kayıt açmaz; ilk cevabı döner.
Idempotency key'i kim üretir?
İstemci üretir. Sunucu üretirse ağ kopunca yeniden denenen istek yeni anahtar alır ve çift işlem açılır. Anahtar, butona her basışta değil, ödeme niyeti oluşurken üretilir.
Ödeme API'sinde neden zorunlu?
Çift tıklama, mobil retry ve gateway timeout aynı POST'u birden fazla kez taşıyabilir. Anahtar yoksa bankaya birden fazla çekim gider. Anahtar varsa ikinci istek ilk sonucu tekrarlar.
Aynı anahtar, farklı gövde gelirse ne olur?
409 Conflict dönülmelidir. Sessizce ilk cevabı vermek tutar manipülasyonunu gizler. Gövde hash'i bu kontrolün teknik karşılığıdır.
Redis mi SQL mi?
Çok instance için Redis kilidi hızlıdır. Mali iz ve mutabakat için SQL kalıcıdır. İkisini birlikte kullanmak yaygındır: Redis rezervasyon, SQL tamamlanmış cevap.
Kaynaklar
- Stripe API: Idempotent requests — anahtar TTL ve çakışma davranışı için fiili endüstri referansı.
- HTTP Semantics (RFC 9110): güvenli ve idempotent yöntem tanımları.