Ana Sayfa / Blog / REST API’de idempotency’yi atlarsanız fatura çifte gider

REST API’de idempotency’yi atlarsanız fatura çifte gider

Ödeme, sipariş, mesaj gönderimi gibi kritik operasyonlarda idempotency şart. Nasıl kurgulayıp neyi atlamamak lazım.

Birkaç yıl önce bir müşterinin ödeme API’si bir sabah %3.2 duplicate charge raporu getirdi. 400’den fazla kullanıcıdan iki kez ücret alınmıştı. Kök sebep basit: bir idempotency eksikliği.

Bunu yaşadıktan sonra her kritik API endpoint’ine idempotency eklemek standart süreç oldu. Bu yazıda pratik bir implementasyon ve atlamamanız gereken incelikler.

Neden gerekli?

İnternet güvenilmez. Client request gönderir, network timeout olur. Client 30 saniye sonra retry eder. Ama ilk istek aslında server’a ulaşmıştı, sadece response client’a dönerken kaybolmuş.

Şimdi backend aynı operasyonu iki kez görecek:

  • Aynı müşteriye 2 kez ödeme
  • Aynı siparişin 2 kez girilmesi
  • Aynı SMS’in 2 kez gönderilmesi

Idempotency bu sorunu önler: client aynı operation key’i ile tekrar deneme yaparsa, server aynı response’u döndürür, ama operasyonu tekrar işlemez.

Pratik implementasyon

Client her unique operasyon için bir Idempotency-Key header’ı üretir (UUID v4). Her retry aynı key ile gelir.

Backend tarafında:

  1. Request geldiğinde header’daki key’i oku
  2. Key için Redis/DB’de önceden kaydedilmiş bir response var mı kontrol et
  3. Varsa, direkt onu dön, operasyonu tekrar işleme
  4. Yoksa, operasyonu işle, sonucu key ile birlikte kaydet (24 saat TTL), response’u dön

Store olarak Redis kullanıyorum, hızlı ve TTL native.

Race condition’u kaçırmayın

Yukarıdaki akış bir problemi ele almıyor: eğer iki istek aynı anda (paralel) gelirse, ikisi de cache’de bulamaz, ikisi de işleme başlar, duplicate ödeme yine olur.

Bunun için distributed lock gerekiyor. Redis’in SETNX komutuyla key üzerine lock alıyorsun, işlem bitene kadar tutuyorsun, bittikten sonra bırakıyorsun. İkinci istek lock’u göremediği için ya bekler ya da 409 döner.

Response content’i de kaydet, sadece success/fail değil

Bu bir incelik ama önemli. Client’a aynı response’u dönmek gerekiyor, sadece “tamam, işlem yapıldı” değil. Eğer ilk request response’unda transaction_id: txn_abc123 döndüyse, retry’de aynı ID’yi görmek gerekiyor.

Dolayısıyla idempotency store’a tam response body’si + status code kaydediliyor, sadece flag değil.

TTL’i doğru ayarla

Idempotency key ne kadar süre tutulmalı?

  • Çok kısa (1 dakika): retry’lar dakikalar sonra gelirse kaçırırsın
  • Çok uzun (1 yıl): storage boşa gider, eski key’lerin çarpışma riski artar

Stripe 24 saat kullanıyor, pratik olarak sağlam bir default. Ben de aynı. Özel bir iş akışın 24 saatten uzun sürecekse, key’i explicit olarak daha uzun tut.

İşe yaramayan yerler, over-engineering

Her endpoint’e idempotency eklemeyin. Gerekli yerler:

  • POST /payments, /charges, /refunds
  • POST /orders, /subscriptions
  • POST /messages, /emails, /sms
  • PUT /accounts (balance değiştiren operasyonlar)

Gerekli değil (idempotent by design):

  • GET endpoints, zaten her aynı sonuç
  • PUT /profile (tüm resource’u replace, aynı veriyle ne kadar çalıştırsan sonuç aynı)
  • DELETE /items/{id} (ilk delete’den sonra 404)

Basit bir kural: operasyon external side effect (ödeme, email, bildirim) üretiyorsa idempotency şart. Sadece DB’yi değiştiriyor ama idempotent bir operasyonsa (user profile update) gerek yok.

Response pattern’i

Idempotency-Key header’ı aldığında response’a da bilgi ekle: Idempotency-Replayed: true header’ı varsa client cache’den geldiğini anlıyor. Debug için faydalı.

Test etmek için

Production’a gitmeden önce: aynı key ile 3 kere art arda istek at. Tümünün aynı response’u dönmesi, side effect’in sadece bir kez oluşması gerekiyor. Integration test’te bu senaryoyu kodlayın.

Sonuç

Idempotency bir “nice to have” değil, kritik API’lerin hijyen standardı. İlk duplicate charge yediğim zaman, artık standart implementation’ım var. Size de öneririm, bir gün olacak, ve o gün hazırlıklı olmak ucuz.

Bu konuda bir projeniz mi var?

Kısa bir özet bırakın, 24 saat içinde size dönüş yapayım.

İletişime Geç