İlginç olan şu ki, Geçen ay, 2025’in Kasım başında İstanbul’da bir fintech demo’su izlerken aynı sahne yine karşıma çıktı: kullanıcı “Öde” düğmesine iki kez bastı, sistem de bunu iki ayrı niyet olarak yorumladı. Can sıkıcı olan şu ki kodun kendisinde teknik bir hata yoktu aslında — mesele, sistemin kararsız anlarda nasıl davrandığıyla ilgiliydi. Hani bazen her şey kağıt üzerinde doğru görünür ama küçük bir gecikme tüm düzeni alt üst eder ya… billing tam olarak öyle bir alan işte.
Doğrusu, Ben bu tıp akışları yıllardır hem SaaS projelerinde hem de ödeme entegrasyonlarında izledim (şaşırtıcı ama gerçek). 2023’te Ankara’daki bir ürün ekibiyle çalışırken, timeout yüzünden ikinci kez tahsilat açan bir servis yüzünden tam üç gün boyunca log kovalamıştık — saatler harcadık, sınır bitti, kahve bitti. O deneyimden sonra şunu net kavradım: ücretlendirme tarafında “doğru kod” yazmak tek başına yetmiyor, deterministik davranış gerekiyor. E peki, sonuç ne oldu? Yani sistem sallansa, ağ kopsa, worker çökse bile sonuç aynı noktaya varmalı (şaşırtıcı ama gerçek)
Peki neden?
Neden Çift Çekim Bu Kadar Kolay Oluyor?
Bence, İşin aslına bakarsanız, ödeme akışı tek bir parçadan oluşmuyor. Bir tarafta istemci var, diğer tarafta veritabanı, sonra dış ödeme sağlayıcısı… Arada ağ kopuyor, yanit gecikiyor, worker bir şekilde çöküyor, kullanıcı da sabredemeyip tekrar tıklıyor (ben de ilk duyduğumda şaşırmıştım). Sonuç? Aynı işlem iki kere tetiklenebiliyor ve sistem bunu zaman zaman meşru iki ayrı istek olarak değerlendiriyor.
Buradaki en sinsi tuzak şu: başarılı ödeme ile başarısız ağ yanitı birbirinden kolayca ayrılabiliyor. Sağlayıcı parayı çekmiş olabilir ama sunucu o anda yanit döndüremiyor. Kullanıcı da gayet haklı olarak tekrar deniyor. Açık konuşayım — bu senaryo teoride bayağı basit görünüyor; pratikte işe gerçekten sınır bozucu.
Geçen sene Berlin’de katıldığım küçük bir SaaS buluşmasında bir mühendis şöyle demişti: “Bizde hata yoktu, retry politikası vardı.” Güldük ama acı gerçek tam buydu. Retry mekanizması yanlış (belki yanilıyorum ama) kurgulanınca güvenlik ağı olmaktan çıkıp bizzat tetikleyiciye dönüşüyor. Düşününce trajikomik bir durum.
Üç Katmanlı Savunma Mantığı
Bence, Bu problemi tek hamlede çözmeye kalkmak genelde hayal kırıklığıyla bitiyor. Kağıt üzerinde güzel duran tekil çözümler pratikte çatlıyor, hatta bazen hiç beklenmedik yerlerde. Daha sağlam bir yaklaşım üç katmandan geçiyor: API sözleşmesi, veritabanı sınırı ve yürütme desenleri (inanın bana). Her biri farklı bir deliği kapatıyor.
Lafı gevelemeden söyleyeyim: API’nız değişken davranıyorsa, veritabanınız tekrarları engellemiyorsa. Worker’larınız yarım bırakılmış işleri güvenli şekilde toparlayamıyorsa — çift çekim ihtimali her zaman masada kalır (ben de ilk duyduğumda şaşırmıştım). Kaçış yok.
| Katman | Amaç | Korumaya Aldığı Risk |
|---|---|---|
| L1 — API sözleşmesi | Değişimi kontrol altına almak | Kırılan istemciler, uyumsuz istekler |
| L2 — DB sınırı | Aynı işi iki kez yazmayı engellemek | Mükerrer kayıtlar, çift çekim |
| L3 — Yürütme deseni | Sallantıda bile aynı sonuca varmak | Retry kaosu, worker çökmesi |
L1: Sözleşme Değişmesin Diye CI’ya Kilit Koymak
Dürüst olmak gerekirse, Bence çoğu ekip ilk darbeyi tam burada yiyor. Birisi endpoint’e yeni bir zorunlu alan ekliyor ya da mevcut bir alanın anlamını sessizce değiştiriyor; ardından mobil uygulama eski sürümden istek atınca ortalık birden karışıyor. Billing gibi hassas alanlarda bu tür “küçük” değişiklikler zincirleme etki yaratıyor — bir domino düşünce diğerleri de gidiyor.
Aslında, Bunu ilk kez 2024 baharında kendi test ortamımda yaşadım. İzmir’deki bir e-ticaret panelinde response şemasına ufak bir alan ekledik diye staging ortamında her şey yeşil kaldı,. Eski istemci patladı. O gün şunu anladım: sözleşme dedikleri şey aslında araç güvenlik kemeri gibi çalışıyor — gevşekse araba gidiyor ama siz içeride savruluyorsunuz. Apple, iWork’te Eski Maç Uygulamalarını Neden Sildi? yazımızda bu konuya da değinmiştik.
{
"contract": {
"required_fields": ["amount", "currency", "idempotency_key"],
"breaking_changes": [
"remove_endpoint",
"change_field_type",
"add_required_without_migration"
]
}
}
Bu tür kontrolleri CI aşamasına gömmek bence fena değil, hatta baya iş görüyor. Bilhassa SaaS ekiplerinde hızlı gelişim uğruna kontratın sessizce bozulması çok yaygın bir şey. Enterprise tarafta işe iş daha sert; orada geriye dönük uyumluluk lüks değil, doğrudan zorunluluk hâline geliyor. Daha fazla bilgi için ChatGPT bir hayatı nasıl raydan çıkardı: Tehlikeli sessizlik yazımıza bakabilirsiniz.
Bunu biraz açayım.
L2: Veritabanı Sınırı Gerçek Fren Görevi Görüyor
API katmanı ne kadar iyi olursa olsun, veritabanında koruma yoksa olay bitmiyor (buna dikkat edin). Asıl kesin çizgiyi DB çekiyor çünkü. UNIQUE constraint burada sıradan bir indeks değil — bildiğin kapıya konmuş turnike gibi çalışıyor, geçirmek isteyeni geçiriyor, ikincisini işe geri çeviriyor.
Ben olsam business identity için tekilleştirme şartını doğrudan tabloda tutarım. Mesela müşteri + sipariş + idempotency anahtarı birlikte benzersiz olması gerekiyorsa bunu uygulama koduna bırakmam; veritabanına emanet ederim. Kod hata yapabilir… DB işe izin vermez. Basit ama güçlü bir ayrım bu. Linux 7.0 Geldi: Numara Değişti, Asıl Hikâye Başka yazımızda bu konuya da değinmiştik.
- Aynı iş için ikinci insert reddedilir. (bu kritik)
- Dış sağlayıcıdan gelen referans ID saklanır.
- Durum geçişleri geri alınamaz hâle getirilir.
- Kayıtlar append-only mantığında tutulursa denetim kolaylaşır. (bu kritik)
Size bir şey söyleyeyim, Küçük startup’ta bu kurgu başta biraz ağır gelebilir, kabul ediyorum. Ama büyüdükçe gerçekten nefes aldırıyor. Kurumsal projede işe böyle bir güvenlik duvarını görmezseniz zaten şaşırırsınız — denetim ekipleri o boşluğu anında yakalar.
Yürütme Desenleri: Wobble Varsa Akış Dağılmasın
Kendi deneyimimden konuşuyorum, Neyse, gelelim en kritik parçaya. Asıl oyun burada dönüyor diyebilirim. Retry geldiğinde sistem aynı charge_id’yi döndürmeli; async iş gecikirse sonuç yine tek noktaya bağlanmalı; worker ölse bile yeniden başladığında iş yarımdan devam etmeli ama yeni bir işlem üretmemeli.
Bu bölüm biraz mutfak gibidir aslında. Malzemeler ayrı ayrı iyi olsa da yemeği pişiren sıra ve ateştir. Ateşi fazla açarsanız taşar. Az açarsanız çiğ kalır. Billing de öyle — idempotency olmadan retry tehlikeli ölür; outbox olmadan dış çağrı ile DB commit’i birbirine dolanır. Doğru kombinasyon lazım. Sihir yok maalesef. Claude’daki “Skills” Neden Prompt Değil, Bağlam Tasarımıdır? yazımızda da bu konuya değinmiştik. Agentic AI: Prompt’tan Özerk Döngülere Geçiş yazımızda da bu konuya değinmiştik.
IDEMPOTENCY-Key Neden Bu Kadar Önemli?
Kullanıcı çift tıkladı diye iki farklı ödeme oluşturulmaması gerekiyorsa niyet kimliğini sabitlemek şart. Idempotency-Key tam olarak bunu yapıyor: aynı anahtar sisteme geldiğinde yeni işlem üretilmiyor, önceki sonuç geri veriliyor. Temiz, net.
Ama dikkat — anahtar metni değil, niyet önemli. Gerçekten önemli mi? Evet, ciddiyim. Çünkü “aynı istek gövdesi” ile “aynı iş niyeti” her zaman birebir örtüşmüyor. Frankfurt’taki bir finans API projesinde çalışan bir arkadaşım sadece request body hash’i kullanmıştı; küçük format farklılıkları yüzünden eşleşmeler kaçmaya başladı, işler karıştı. Sonra business key’e geçince sorunların yarısı uçup gitti. Kulağa basit geliyor ama fark gerçekten büyük — bunu yaşamadan tam anlamıyor insan.
INSERT INTO charges (customer_id, order_id, idempotency_key, amount)
VALUES ($1, $2, $3, $4)
ON CONFLICT (customer_id, order_id) DO UPDATE
SET updated_at = NOW()
RETURNING charge_id;
Outbox ile Dış Servisleri Kontrol Altına Almak
Dış ödeme sağlayıcısını doğrudan transaction içine sokmaya çalışmak çoğu zaman sıkıntı çıkarıyor. DB commit oldu mu olmadı mı derken ağ tarafındaki belirsizlik tüm resmî dağıtıyor. Bu yüzden ben genelde önce DB’ye charge kaydıyla birlikte outbox yazarım, sonra ayrı bir worker dış servisi çağırır. At-least-once modelinin içinden çıkmanın yolu bu (ciddiyim)
Bir de şu var: sağlayıcının kendi idempotency davranışı sizin beklentinizle birebir örtüşmeyebilir. Bazısı header ister, bazısı body, bazısı hiç umursamaz gibi davranır — evet, maalesef böyle sağlayıcılar var. O yüzden kendi kontrolünüzü elden bırakmayın, dış dünyaya fazla güvenmeyin.
“Billing’de başarıyı ölçmenin en iyi yolu ‘kaç işlem yaptık?’ değildir; ‘kaç tanesini yanlışlıkla iki kere yapmadık?’ sorusudur.”
Küçük Startup ile Büyük Kurum Aynı Şeyi Yaşamaz mı?
Kısmen yaşar ama etkisi çok farklı ölür. Küçük startup’ta sorun genelde hızla büyür: iki yanlış tahsilat müşteri desteğine düşer, birkaç gün içinde itibar yerle bir olabilir. Enterprise seviyede işe etki daha bürokratik ilerliyor: geri ödeme süreci, uyum raporları, mutabakat dosyaları… Hepsi birbirine giriyor, çözmesi haftalar alıyor.
Ben açıkçası küçük ekiplerde sadelikten yana olurum — ama sade olmak başka, gevşek olmak bambaşka bir şey. Mesela startup için minimum set şu olabilir: idempotency key, UNIQUE constraint, temel outbox worker. Kurumsalda bunlara ek olarak audit ledger, state machine doğrulaması ve schema contract testi de gerekiyor. Tek ülkeye satış yapıyorsanız bir hata belki affedilebilir gibi görünür; beş ülkede ödeme alıyorsanız aynı hata hukukî masrafa dönüşüyor. E hâliyle mesele büyüyor.
Bana Göre En Sağlam Pratik Kombinasyon Ne?
Sanırım şöyle özetlemek en doğrusu:
- İstemciden idempotency key işte — bunu es geçmeyin
- Veritabanında unique constraint uygula (bu kritik)
- Outbox ile dış sistemi ayır
- Worker retry politikasını kontrollü tut
- Sağlayıcı yanitlarını reconciliation için sakla
Bu beşi birlikte kullandığınızda sistem mucizevi biçimde kusursuz olmaz — beklenti bu olmasın zaten. Ama çift çekim ihtimalini ciddi şekilde aşağı çekiyor. Aslında dür bir saniye, önce şunu söyleyeyim: hiçbir desen tek başına kurtarıcı değil. Biri eksik kalınca zincirin o halkası kopuyor; özellikle yoğun trafik altında bu boşluk hemen kendini belli ediyor. Kombinasyon önemli, sıra önemli, tutarlılık önemli. Gerisi detay.
Sıkça Sorulan Sorular
Billing’de “çifte ücret” neden oluyor?
Genelde tek bir hatadan değil, farklı parçaların aynı anda “farklı anlamlar” üretmesinden oluyor: istemci tekrar tıklıyor, ağ gecikiyor, worker zamanında yanit alamıyor ve sistem aynı ödemeyi iki ayrı istek gibi işleyebiliyor. Özellikle başarılı ödeme ile “başarılı ama yanit dönmedi” senaryosu karışınca süreç zorlaşıyor. Benzer bir problemi SaaS tarafında yaşayınca, bunun çoğu zaman deterministiklik eksikliğinden kaynaklandığını net gördüm.
Retry (yeniden deneme) politikası çifte tahsilatı nasıl tetikler?
Retry doğru yerde kullanılırsa kurtarıcıdır; ama yanlış kurgulanırsa güvenlik ağı olmaktan çıkıp tetikleyiciye dönüşür. Örneğin zaman aşımı yüzünden “işlem başarısız” sanılıp tekrar denendiğinde, sağlayıcı aslında ilk denemede tahsilatı tamamlamış olabilir. Bu durumda ikinci deneme de fiziksel olarak ikinci bir tahsilata gider.
“Aynı isteği tekrar işleme”yi nasıl engellerim?
En pratik yol, istekleri idempotency (eşsiz istek kimliği) ile birleştirmek ve aynı kimlik için yalnızca tek sonuç döndürmektir. Yani API sözleşmesinde istemciden bir idempotency key istemek, veritabanında bunu benzersiz kısıtla garanti altına almak ve worker’ların da bu kurala uymasını sağlamak gerekir. Benim ekiplerde en çok işe yarayan yaklaşım, “tek katmanla değil, üç katmanla” düşünmek oldu.
İşlem başarılıysa ama benim sistem “başarısız” diyorsa ne yapmalıyım?
Bu durumda kritik olan, sağlayıcıdan gelen kesin durumu kaynak kabul edip kendi durumunu ona göre düzeltmektir. Örneğin webhook’lar ve/veya sağlayıcıdan periyodik doğrulama ile “gerçek durum” senkronize edilir. Böylece kullanıcıya dönen yanit gecikse bile sistem sonunda aynı noktaya varır.
Worker çökmesi (crash) billing’de neden risk oluşturur?
Worker yarım bir adımda kalınca, aynı ödeme tekrar tetiklenebilir veya durum güncellemesi yapılmadan süreç yeniden başlayabilir. Bu yüzden “yarım kalan işleri güvenli toparlama” ve durum geçişlerini atomik/izlenebilir yapmak önemlidir. Pratikte, DB’de benzersiz kısıt + outbox/webhook senaryoları bu riski ciddi azaltır.
Kaynaklar ve İleri Okuma
Idempotent Operations (Azure Architecture Center)
Transactional Outbox Pattern (Azure Architecture Center)
Asynchronous Message Processing / Async Communication (Azure Architecture Center)
Stripe Webhooks (resmî dokümantasyon)
Bu içerik işinize yaradı mı?
Benzer içerikleri kaçırmamak için beni sosyal medyada takip edin.



