İçeriğe atla

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:

PaymentsAPIClientPaymentsAPIClientClient retry yaparMusteri iki kez odediPOST /charge $50Karti cekBasariliYanit kayboldu (timeout)POST /charge $50Karti yine cekBasariliBasarili

İ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:

MetotSafeIdempotentTipik Kullanım
GETEvetEvetVeri okuma
PUTHayırEvetTam değiştirme
DELETEHayırEvetKaynak silme
POSTHayırHayırYeni kaynak oluşturma
PATCHHayırDuruma göreKı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ı.

Hit

Miss

Client UUID uretir

Istek key ile gonderilir

Sunucu store kontrolu

Cache'lenmis yanit dondur

Istegi isle

Yaniti TTL ile sakla

Yaniti dondur

Nasıl Çalışır

  1. Client, isteği göndermeden önce bir UUID üretir.
  2. Client onu bir Idempotency-Key header’ında gönderir.
  3. Sunucu, hızlı bir store’da (Redis, DynamoDB) bu key’i arar.
  4. Key yeniyse sunucu isteği işler ve tam yanıtı TTL ile saklar.
  5. 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.

DedupStoreConsumerQueueProducerDedupStoreConsumerQueueProducermesaj(id=abc)teslim(abc)abc goruldu mu?HayirIsleabc isaretleack kaybolduyeniden teslim(abc)abc goruldu mu?Evetack (atla)

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-Key header’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

İlgili yazılar

Web ve Mobil için Asenkron API Desenleri: Görüşlü Bir Varsayılan

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.

api-designwebsocketsserver-sent-events+5
TypeScript AI SDK Karşılaştırması: Agent Geliştirme için Vercel AI SDK vs OpenAI Agents SDK

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.

typescriptai-toolsserverless+4
Transactional Outbox Pattern: Dağıtık Sistemlerde Güvenilir Event Publishing

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.

distributed-systemsmicroservicesevent-driven+7
Custom MCP Server Geliştirme: Production-Ready Kılavuz

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.

typescriptmcpnodejs+5
Saga Pattern ile Dağıtık Transaction'lar: ACID Olmadan Consistency Sağlamak

Microservices mimarisinde AWS Step Functions ve EventBridge kullanarak Saga pattern implementasyonu: idempotency, compensation logic ve production-ready pattern'ler.

saga-patterndistributed-systemsmicroservices+5