2026-04-09
Idempotency: API'lerde Güvenli Retry için Başlangıç Rehberi
API, ödeme akışı ve mesaj tüketicisi geliştiren yazılımcılar için idempotency'ye pratik bir giriş. HTTP metot semantiği, idempotency key'leri, veritabanı upsert ve yaygın tuzakları çalışan Node.js örnekleriyle anlatır.
Idempotency, aynı işlemin defalarca çalıştırılmasının bir kez çalıştırılmasıyla aynı sonucu üretmesi özelliğidir. Para, yan etki ya da dağıtık iş tutan her API için kritiktir; çünkü altta yatan taşıma katmanları (HTTP, mobil radyolar, mesaj aracıları) başarısızlık durumunda niyetten bağımsız olarak retry yapar. Idempotent olmayan bir endpoint, her retry’ı bir duplicate yazmaya çevirir: çift ücretlendirme, tekrarlanan siparişler, iki kez işlenen işler.
Bu rehber; API, ödeme akışı veya mesaj tüketicisi geliştiren yazılımcılar içindir. HTTP metot semantiğini, Stripe ve benzeri sistemlerin kullandığı idempotency key desenini, veritabanı seviyesinde upsert’ü ve güvenli görünen idempotency’yi sessiz bir bug kaynağına çeviren tuzakları (cache uyumsuzlukları, anahtar çakışmaları, deterministik olmayan yan etkiler) ele alır.
Idempotency Ne Demek
Bir işlem idempotent ise, onu birden fazla kez çalıştırmak bir kez çalıştırmakla aynı sonucu üretir. Matematiksel tanımı f(f(x)) = f(x). Pratikte: aynı endpoint’i aynı girdiyle iki kez çağırırsanız sistem, tek çağırmışsınız gibi aynı duruma varır.
İncelik burada. Idempotency, duplicate isteklerin sisteme ulaşmasını engellemek değildir. Duplicate isteklerin duplicate etki oluşturmamasını sağlamaktır. Ağ, siz istesiniz ya da istemeyin retry yapacak. Sizin göreviniz buna tolerans göstermek.
Üç İlgili Terim
- Safe: hiç yan etki yok (GET isteği)
- Idempotent: yan etki oluşur ama tekrarlar yeni bir şey eklemez (PUT, DELETE)
- Pure: deterministik, yan etkisiz, dış state’e bağımsız (matematik fonksiyonları)
Her safe işlem idempotent’tır. Her idempotent işlem safe değildir.
Neden Önemli: Çift Ücretlendirme Problemi
Bir ödeme akışını düşünün:
İlk ödeme sunucuda başarıyla gerçekleşti ama yanıt client’a hiç ulaşmadı. Client retry yaptı ve müşteri iki kez ücretlendirildi. Sunucunun, ikinci isteğin birincinin retry’ı olduğunu anlamasının bir yolu yoktu.
Çözüm bir idempotency key: client’ın bir kez üretip aynı mantıksal işlemin tüm retry’larında yeniden kullandığı benzersiz bir ID.
HTTP Metotları ve Idempotency
RFC 9110, standart HTTP metotları için sözleşmeyi tanımlar:
| Metot | Safe | Idempotent | Tipik Kullanım |
|---|---|---|---|
| GET | Evet | Evet | Veri okuma |
| PUT | Hayır | Evet | Tam değiştirme |
| DELETE | Hayır | Evet | Kaynak silme |
| POST | Hayır | Hayır | Yeni kaynak oluşturma |
| PATCH | Hayır | Duruma göre | Kısmi güncelleme |
Aynı gövdeyle üç kez çağrılan bir PUT /users/123, kullanıcı kaydını aynı durumda bırakır. İki kez çağrılan DELETE /orders/456, siparişin silinmiş olmasıyla sonuçlanır. Ama POST /orders her seferinde yeni bir sipariş oluşturur, dolayısıyla varsayılan olarak idempotent değildir.
Bu önemli çünkü tarayıcılar, proxy’ler ve HTTP client’ları GET, PUT ve DELETE isteklerini otomatik retry edebilir, güvenli olduklarını varsayarlar. PUT uygun olan yerde POST kullanırsanız bu garantiyi kaybedersiniz.
Idempotency Key Deseni
Bu, POST işlemlerini idempotent yapmanın standart yolu ve Stripe tarafından yaygınlaştırıldı.
Nasıl Çalışır
- Client, isteği göndermeden önce bir UUID üretir.
- Client onu bir
Idempotency-Keyheader’ında gönderir. - Sunucu, hızlı bir store’da (Redis, DynamoDB) bu key’i arar.
- Key yeniyse sunucu isteği işler ve tam yanıtı TTL ile saklar.
- Key zaten varsa sunucu, iş mantığını yeniden çalıştırmadan saklanan yanıtı döndürür.
Minimal Bir Express Uygulaması
import express, { Request, Response, NextFunction } from "express";
import { createClient } from "redis";
import { randomUUID } from "crypto";
const redis = createClient();
await redis.connect();
interface StoredResponse {
status: number;
body: unknown;
}
async function idempotency(req: Request, res: Response, next: NextFunction) {
const key = req.header("Idempotency-Key");
if (!key) return next();
// Tenant'lar arası çakışmayı önlemek için kullanıcı ve endpoint ile scope
const storeKey = `idem:${req.user?.id}:${req.path}:${key}`;
const cached = await redis.get(storeKey);
if (cached) {
const stored: StoredResponse = JSON.parse(cached);
return res.status(stored.status).json(stored.body);
}
// Eşzamanlı retry'ları yönetmek için key'i 30 saniye kilitle
const locked = await redis.set(storeKey + ":lock", "1", {
NX: true,
EX: 30,
});
if (!locked) {
return res.status(409).json({ error: "Request in progress" });
}
// Yanıtı yakala ki saklayabilelim
const originalJson = res.json.bind(res);
res.json = (body: unknown) => {
const toStore: StoredResponse = { status: res.statusCode, body };
// 24 saatlik TTL, Stripe'ın varsayılanı ile aynı
redis.set(storeKey, JSON.stringify(toStore), { EX: 86400 });
return originalJson(body);
};
next();
}
Bu middleware temelleri ele alıyor: kullanıcıya göre scope, eşzamanlılık için kilit, tam yanıtı saklama ve retry’da tekrar oynatma. Production sistemleri genelde daha fazlasını ekler: işleniyor/tamamlandı ayrımı, request body’nin saklanan key ile eşleştiğini doğrulama ve daha zengin hata yönetimi.
Client Tarafı
import { randomUUID } from "crypto";
async function chargeCustomer(amount: number) {
const idempotencyKey = randomUUID();
// Aynı key'i bu mantıksal işlemin tüm retry'larında yeniden kullan
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch("/api/charge", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ amount }),
});
if (res.ok) return res.json();
} catch (err) {
// Ağ hatası, aynı key ile retry
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
}
}
throw new Error("Charge failed after retries");
}
Önemli nokta: client key’i bir kez, ilk denemeden önce üretir ve her retry’da aynısını kullanır. Her denemede yeni bir UUID üretmek, tüm deseni geçersiz kılar.
Veritabanı Seviyesinde Idempotency
Bazen bu işi veritabanı sizin için yapabilir. Unique constraint’ler ve conditional write’lar, neredeyse hiç uygulama kodu olmadan idempotency sağlar.
PostgreSQL Upsert
INSERT INTO orders (id, user_id, amount, created_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (id) DO NOTHING
RETURNING *;
Çağıran taraf sipariş ID’sini sağlıyorsa bunu iki kez çalıştırmak tabloda aynı satırı bırakır. İkinci çağrı DO NOTHING yüzünden hiçbir şey döndürmez ve handler’ınız bunu başarılı bir no-op olarak değerlendirebilir.
DynamoDB Conditional Write
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});
async function createOrder(orderId: string, data: Record<string, string>) {
try {
await client.send(
new PutItemCommand({
TableName: "Orders",
Item: {
id: { S: orderId },
data: { S: JSON.stringify(data) },
},
ConditionExpression: "attribute_not_exists(id)",
}),
);
return { created: true };
} catch (err: any) {
if (err.name === "ConditionalCheckFailedException") {
// Zaten var, başarılı say
return { created: false };
}
throw err;
}
}
attribute_not_exists koşulu write’ın sadece ilk seferde başarılı olmasını sağlar. Retry’lar catch bloğuna düşer ve no-op olur.
Mesaj Kuyruklarında Idempotency
Kuyrukların çoğu exactly-once değil, at-least-once teslimat garantisi verir. SQS, Kafka, RabbitMQ ve Pub/Sub; hepsi aynı mesajı birden fazla kez teslim edebilir. Tüketiciler ack atmadan önce çöker, visibility timeout’lar dolar, producer’lar retry yapar. Tüketiciniz replay’leri tolere etmek zorunda.
Mesaj ID’si ile anahtarlanan, retention penceresinden biraz uzun TTL’li basit bir dedup tablosu genelde yeterli. Daha güçlü garantiler için yan etkiyi ve “işlendi” işaretini aynı veritabanı transaction’ında birleştirin (outbox deseni).
Exactly-Once Bir Mit
Dağıtık sistemlerde exactly-once teslimat imkansızdır. Bu teorik bir sonuç (İki Generaller Problemi, daha fazlası için okuyabilirsiniz). Başarabileceğiniz şey, at-least-once teslimat ile idempotent handler’ları birleştirerek exactly-once işleme:
at-least-once teslimat + idempotent isleme = etkili olarak exactly-once
Kafka’nın “exactly-once semantics”i Kafka ekosistemi içinde çalışır; ama bir e-posta gönderdiğiniz veya dış bir API çağırdığınız anda yine idempotent handler’lara ihtiyacınız var.
Yaygın Tuzaklar
Pratikte idempotency’yi bozan hatalar bunlar.
1. Handler İçinde Wall-Clock Time Kullanmak
Handler’ınız her çağrıda created_at = NOW() hesaplıyorsa, saklanan satırlar ilk çağrı ile retry arasında farklılaşır. İşlem artık katı anlamda idempotent değildir.
Çözüm: timestamp’leri bir kez yakalayın ve idempotency key ile birlikte saklayın ya da parametre olarak geçirin.
2. Idempotency Sınırı Dışındaki Yan Etkiler
Yaygın bir desen: önce veritabanına commit, sonra e-posta gönder. E-posta gönderimi başarısız olup client retry yaparsa veritabanı write’ı iki kez gerçekleşir (korunmuyorsa) ya da e-posta iki kez gider (korunuyorsa).
Çözüm: e-posta niyetini aynı transaction içinde veritabanına yazın ve ayrı bir worker’ın idempotent şekilde teslim etmesini sağlayın.
3. Idempotency Key Olarak Timestamp
user-123-1696000000 gibi key’ler yük altında çakışır ve saat kayması altında bozulur. UUID v4 veya v7 kullanın. Yalnızca wall-clock time’a asla güvenmeyin.
4. Yanıt Gövdesini Saklamayı Unutmak
Bir key’i “işlendi” olarak işaretleyip yanıtı saklamamak, replay yapamamanıza yol açar. Retry ya hata verir ya da mantığı yeniden çalıştırır.
Çözüm: tam yanıtı (status, header, body) işlendi işareti ile atomik olarak saklayın.
5. Eşzamanlılığı Yok Saymak
Aynı key ile iki eşzamanlı istek, ikisi de cache’i ıskalar, ikisi de handler’ı çalıştırır, ikisi de sonuç saklar. Biri kazanır ama her iki yan etki de gerçekleşmiştir.
Çözüm: “işleniyor olarak işaretle” adımında key üzerinde kilit ya da unique constraint kullanın.
6. Tenant’lar Arasında Key Sızıntısı
Scope’suz global bir key store, bir müşterinin key’inin başkasının işlemiyle eşleşmesine izin verir.
Çözüm: key’leri tenant_id:user_id:endpoint:key olarak scope’layın.
7. Çok Kısa TTL
Key’ler, client pes etmeden önce sona ererse geç bir retry ikinci bir execution’a yol açar.
Çözüm: herhangi makul bir retry penceresinden daha uzun bir TTL seçin. 24 saat makul bir varsayılan.
Ne Zaman Neyi Kullanmalı
- Salt okunur endpoint: GET kullanın. Başka bir şey gerekmiyor.
- Tam değiştirme: PUT kullanın. Sözleşme gereği idempotent.
- Kaynak silme: DELETE kullanın. Sözleşme gereği idempotent.
- Client’ın bildiği ID ile oluşturma: PUT kullanın ya da ID üzerinde unique constraint olan POST.
- Sunucunun ürettiği ID ile oluşturma:
Idempotency-Keyheader’lı POST kullanın. - Mesaj kuyruğu tüketicisi: dedup tablosu ya da outbox deseni, her zaman.
- Ödeme, sipariş, e-posta: idempotency key’leri pazarlık dışıdır.
Sonuç
Idempotency, ölçeklenmeye başladığınızda öğreneceğiniz ileri bir konu değil. Retry mantığı olan her API için temel bir gereksinimdir ki bu da her API demek. HTTP metotlarını doğru seçin, gerçek dünya yan etkileri olan POST endpoint’lerine idempotency key ekleyin, mümkün olan yerde veritabanı constraint’lerini kullanın ve kuyruk tüketicilerinizi replay’leri tolere edecek şekilde tasarlayın.
Desenler karmaşık değil. Önemli olan, ilk çift ücretlendirmenizden sonra değil, öncesinde bunları bilinçli uygulamak.
Kaynaklar
- RFC 9110: HTTP Semantics - Idempotent Methods - Hangi metotların idempotent olduğunu tanımlayan resmi HTTP spesifikasyonu
- MDN Web Docs: Idempotent - HTTP metot idempotency’sinin anlaşılır açıklaması
- Stripe API Documentation: Idempotent Requests - Ödeme API’sinde idempotency key’lerinin kanonik örneği
- Stripe Engineering: Designing Robust and Predictable APIs - Stripe’ın iç implementasyonuna derinlemesine bakış
- AWS Lambda Powertools: Idempotency Utility - Lambda için DynamoDB tabanlı production kütüphanesi
- AWS SQS FIFO: Exactly-Once Processing - Deduplication üzerine AWS dokümantasyonu
- IETF Draft: The Idempotency-Key HTTP Header Field - Header için gelişen standart
- PostgreSQL Documentation: INSERT ON CONFLICT - Idempotent insert’ler için upsert söz dizimi
- DynamoDB Conditional Writes - Idempotent write’lar için condition expression’lar
- Apache Kafka: Semantics and Idempotent Producer - Exactly-once semantics’in sınırları
- The Two Generals Problem - Exactly-once teslimatın neden imkansız olduğu
- Square Developer Docs: Idempotency - Başka bir ödeme sağlayıcısından alternatif bakış
İlgili yazılar
Tek bir backend üzerinde çalışan web SPA ve mobil uygulama için uzun süreli işlere dair tek bir varsayılan desen ve onu geçersiz kılmanız gereken durumlar.
AI agent geliştirmek için TypeScript SDK'larının pratik karşılaştırması - Vercel AI SDK, OpenAI Agents SDK ve AWS Bedrock entegrasyonu. Kod örnekleri, karar frameworkleri ve production patternleri içeriyor.
Transactional Outbox Pattern'in dağıtık sistemlerdeki dual-write problemini nasıl çözdüğünü, PostgreSQL, DynamoDB ve CDC araçlarıyla pratik implementasyonlarını öğren.
TypeScript ile organizasyonunuzun internal sistemleri için custom Model Context Protocol serverları nasıl geliştirip, güvenli hale getirip, deploy edeceğinizi öğren. Authentication, monitoring ve Kubernetes deployment örnekleriyle.
Microservices mimarisinde AWS Step Functions ve EventBridge kullanarak Saga pattern implementasyonu: idempotency, compensation logic ve production-ready pattern'ler.