Pat Helland'ın "dışarıdaki vs. içerideki veri" fikrini öğrenin: net sınırlar belirleyin, idempotent çağrılar tasarlayın ve ağ hatalarında durumu uzlaştırmayı planlayın.

Bir uygulama kurarken isteklerin düzgünce, tek tek ve doğru sırada geldiğini hayal etmek kolaydır. Gerçek ağlar böyle çalışmaz. Kullanıcı ekran donduğu için "Öde"ye iki kez tıklar. Mobil bağlantı düğmeden hemen sonra kopar. Bir webhook geç gelir ya da iki kez gelir. Bazen hiç gelmez.
Pat Helland'ın dışarıdaki vs içerideki veri fikri bu karmaşayı düşünmenin net bir yoludur.
“Dışarı” sisteminizin kontrol etmediği her şeydir. Başkalarıyla ve diğer sistemlerle konuştuğunuz yer, teslimatın belirsiz olduğu yer: tarayıcılardan ve mobil uygulamalardan gelen HTTP istekleri, kuyruk mesajları, üçüncü taraf webhook'lar (ödeme, e‑posta, gönderim) ve istemciler, proxy'ler veya arka plan işleri tarafından tetiklenen yeniden denemeler.
Dışarıda mesajların gecikebileceğini, çoğaltılabileceğini veya yanlış sırada gelebileceğini varsayın. "Genelde güvenilir" olsa bile bir gün güvenilmez olacağını tasarlayın.
“İçeride” sisteminizin güvenilir kılabildiği şeydir. Sakladığınız dayanıklı durum, uyguladığınız kurallar ve daha sonra kanıtlayabileceğiniz gerçekler şunlardır:
İçeride değişmezlikleri (invariants) korursunuz. "Sipariş başına bir ödeme" diye söz veriyorsanız, bu söz içeride uygulanmalıdır çünkü dışarı güvenilemez.
Zihniyet değişimi basittir: mükemmel teslimat veya mükemmel zamanlama varsaymayın. Her dış etkileşimi tekrar edilebilecek güvenilmez bir öneri gibi ele alın ve içeriğin buna güvenli şekilde tepki vermesini sağlayın.
Bu küçük ekipler ve basit uygulamalar için bile önemlidir. Bir ağ sorunu ilk kez çift ücretlendirme veya takılı bir sipariş yarattığında, bu teori olmaktan çıkar ve iade, destek bileti ve güven kaybı olur.
Somut bir örnek: kullanıcı "Siparişi ver" diyor, uygulama bir istek gönderiyor ve bağlantı kopuyor. Kullanıcı tekrar deniyor. İçeride "bu aynı deneme" olduğunu tanıma yolunuz yoksa iki sipariş oluşturabilir, stoğu iki kez rezerve edebilir veya iki onay e‑postası gönderebilirsiniz.
Helland'ın noktası açık: dış dünya belirsizdir, ama sisteminizin içi tutarlı kalmalıdır. Ağ paketleri düşer, telefonlar sinyal kaybeder, saatler kayar ve kullanıcılar yenileyebilir. Uygulamanız bunların hiçbirini kontrol edemez. Kontrol edebileceği şey, verinin net bir sınırı aşıp içeride "doğru" olarak kabul edildiği zamandır.
Birinin kötü Wi‑Fi olan bir binada yürürken telefondan kahve siparişi verdiğini hayal edin. "Öde"ye tıklar. Dönen yükleniyor göstergesi görünür. Ağ kopar. Tekrar tıklar.
Belki ilk istek sunucunuza ulaştı ama yanıt geri dönmedi. Ya da belki hiçbiri ulaşmadı. Kullanıcının gözünden her iki olasılık da aynı görünür.
İşte zaman ve belirsizlik: henüz ne olduğunu bilmiyorsunuz ve sonra öğrenebilirsiniz. Sisteminiz beklerken makul davranmalı.
Dışarının güvenilmez olduğunu kabul ettikten sonra bazı “garip” davranışlar normalleşir:
Dış veri bir iddiadır, bir gerçek değil. "Ödedim" demek, güvenilmez bir kanal üzerinden gönderilen bir beyanattır. İçeride kalıcı, tutarlı şekilde kaydettiğinizde gerçek olur.
Bu sizi üç pratik alışkanlığa iter: net sınırlar tanımlayın, yeniden denemeleri idempotent yapın ve gerçeklik uyuşmadığında uzlaşma planlayın.
“Dışarı vs içeride” fikri pratik bir soruyla başlar: sisteminizin doğruluğu nerede başlar ve nerede biter?
Sınırın içinde güçlü garantiler verebilirsiniz çünkü veriyi ve kuralları siz kontrol edersiniz. Sınırın dışında ise en iyi çabayı gösterir ve mesajların kaybolabileceğini, çoğaltılabileceğini, gecikebileceğini veya yanlış sırada gelebileceğini varsayarsınız.
Gerçek uygulamalarda bu sınır genellikle şu yerlerde görünür:
O çizgiyi çektikten sonra hangi değişmezliklerin tartışılamaz olduğunu karar verin. Örnekler:
Sınır ayrıca "nerede olduğumuz" için net bir dil gerektirir. Birçok hata "sizi duyduk" ile "bitirdik" arasındaki boşlukta yaşar. Yararlı bir desen üç anlamı ayırmaktır:
Takımlar bunu atladığında yalnızca yük altında veya kısmi kesintiler sırasında meydana gelen hatalar çıkar. Bir sistem "ödendi" derken paranın çekildiğini; bir diğeri ödeme girişiminin başlatıldığını kastedebilir. Bu uyumsuzluk çiftler, takılı siparişler ve kimsenin çoğu kez yeniden üretemediği destek biletleri yaratır.
Idempotency şu anlama gelir: aynı istek iki kez gönderilse bile sistem onu tek bir istekmiş gibi ele alır ve aynı sonucu döndürür.
Yeniden denemeler normaldir. Zaman aşımı olur. İstemciler kendilerini tekrarlar. Dışarı tekrar edebiliyorsa, içeri bunun sonucunu sabit duruma dönüştürmelidir.
Basit bir örnek: bir mobil uygulama "20$ öde" gönderir ve bağlantı kopar. Uygulama yeniden dener. Idempotency yoksa müşteri iki kez ücretlendirilebilir. Idempotency ile ikinci istek ilk ücretlendirme sonucunu döndürür.
Çoğu ekip şu desenlerden birini (bazen karışık) kullanır:
Idempotency-Key: ...). Sunucu anahtarı ve nihai yanıtı kaydeder.Bir kopya geldiğinde en iyi davranış genellikle "409 conflict" veya genel bir hata değil; ilk kez verdiğinizle aynı sonucu geri döndürmektir; aynı kaynak kimliği ve durum dahil. Bu, istemciler ve arka plan işleri için yeniden denemeleri güvenli kılar.
Idempotency kaydı sınırınız içinde kalıcı depolamada olmalıdır, bellekte değil. API'niz yeniden başlarsa ve unutursa, güvenlik garantisi ortadan kalkar.
Kayıtları gerçekçi yeniden denemeleri ve gecikmiş teslimleri kapsayacak kadar uzun tutun. Pencere iş riskine bağlıdır: düşük riskli yaratımlar için dakikalar–saatler, çift maliyeti olan ödemeler/e‑postalar/gönderimler için günler ve partnerlerin uzun süre yeniden deneyebileceği durumlarda daha uzun.
Dağıtık işlemler teselli edici gelebilir: servisler, kuyruklar ve veritabanları arasında tek bir büyük commit. Pratikte genellikle kullanılamaz, yavaştır veya güvenilmezdir. Bir ağ sıçraması olduğunda her şeyin birlikte commit edildiğini varsayamazsınız.
Yaygın bir tuzak her adımın hemen şimdi başarılı olmasıyla çalışan bir iş akışı inşa etmektir: siparişi kaydet, kartı çek, stoğu rezerve et, onay gönder. 3. adım zaman aşımına uğradığında başarısız mı oldu yoksa oldu mu? Yeniden denersem çift ücretlendirir veya çift rezerve eder miyim?
Bunu önlemenin iki pratik yolu vardır:
Her iş akışı için bir stil seçin ve ona sadık kalın. "Bazen outbox kullanıyoruz" ile "bazen senkron başarı varsayıyoruz"u karıştırmak test edilmesi zor kenar durumlar yaratır.
Basit bir kural yardımcı olur: sınırların ötesinde atomik olarak commit edemiyorsanız, yeniden denemeler, kopyalar ve gecikmeler için tasarlayın.
Uzlaşma temel bir gerçeği kabul etmektir: uygulamanız ağ üzerinden diğer sistemlerle konuştuğunda bazen ne olduğunu konusunda anlaşmazlık olacaktır. İstekler zaman aşımına uğrar, callback'ler geç gelir ve insanlar eylemleri yeniden dener. Uzlaşma, uyuşmazlıkları tespit edip zaman içinde düzeltme yönteminizdir.
Dış sistemleri bağımsız gerçek kaynakları olarak ele alın. Uygulamanız kendi iç kaydını tutar, ama partnerlerin, sağlayıcıların ve kullanıcıların aslında ne yaptığını karşılaştırma yolunuz olmalıdır.
Çoğu ekip küçük ama etkili araçlar kullanır (sıkıcı olmak iyidir): bekleyen eylemleri yeniden deneyen bir worker, tutarsızlıklar için zamanlanmış bir tarama ve destek ekibinin yeniden denemesi, iptal etmesi veya incelendi olarak işaretlemesi için küçük bir onarım aracı.
Uzlaşma yalnızca neyi karşılaştıracağınızı biliyorsanız işe yarar: iç defter vs sağlayıcı defteri (ödemeler), sipariş durumu vs gönderim durumu (fulfillment), abonelik durumu vs faturalama durumu.
Durumları onarılabilir yapın. "created"dan doğrudan "completed"a atlamak yerine, bekleme gibi ara durumlar kullanın. Bu "emin değiliz" demeyi güvenli kılar ve uzlaşmanın iniş yapacağı net bir yer verir.
Önemli değişikliklerde küçük bir denetim izi yakalayın:
Örnek: uygulamanız bir gönderim etiketi isterken ağ koparsa, içerde "etiket yok" olabilir ama taşıyıcı aslında bir etiket oluşturmuş olabilir. Bir recon worker korelasyon ID'si ile arama yapıp etiketi bulabilir ve siparişi ilerletebilir (veya ayrıntılar uyuşmuyorsa inceleme için işaretleyebilir).
Ağı başarısız olacak varsayımıyla hedef değişir. Amacınız artık her adımın tek denemede başarılı olması değil. Amacınız her adımı tekrar etmek için güvenli ve onarımı kolay yapmak.
Bir cümlelik bir sınır beyanı yazın. Sisteminizin neyin kaynağı olduğunu, neyi yansıttığınızı ve yalnızca başkalarından ne istediğinizi açıkça belirtin.
Mutluluk yolundan önce hata modlarını listeleyin. En azından: zaman aşımı (işleyip işlemediğini bilmiyoruz), tekrar eden istekler, kısmi başarı (bir adım oldu, sonraki olmadı) ve sırasız olaylar.
Her girdi için bir idempotency stratejisi seçin. Senkron API'ler için genelde idempotency anahtarı artı saklanan sonuç; mesajlar/olaylar için genelde benzersiz mesaj ID'si ve "bunu işledim mi?" kaydı.
Niyeti kalıcı hale getirip sonra harekete geçin. Önce PaymentAttempt: pending veya ShipmentRequest: queued gibi dayanıklı bir şey saklayın, sonra dış çağrıyı yapın, ardından sonucu kaydedin. Yeniden denemelerin aynı niyete işaret etmesi için stabil bir referans ID döndürün.
Uzlaşma ve onarım yolunu oluşturun ve görünür hale getirin. Uzlaşma "çok uzun süredir bekleyen" kayıtları yeniden kontrol eden bir iş olabilir. Onarım eylemi güvenli bir admin işlemi (yeniden dene, iptal et veya çözülmüş olarak işaretle) ve bir denetim notu olabilir. Temel gözlemlenebilirlik ekleyin: korelasyon ID'leri, net durum alanları ve birkaç sayaç (bekleyen, yeniden deneme, başarısızlık).
Örnek: checkout zaman aşımına uğradıktan hemen sonra ödeme sağlayıcısına çağrı yaptıysanız, tahminde bulunmayın. Denemeyi saklayın, deneme ID'si döndürün ve kullanıcı aynı idempotency anahtarıyla yeniden denerse aynı denemeye işaret etsin. Daha sonra uzlaşma sağlayıcının ücretleyip ücretlemediğini doğrulayabilir ve denemeyi çift ücretlendirmeden güncelleyebilir.
Müşteri "Siparişi ver"e tıklar. Servisiniz sağlayıcıya bir ödeme isteği gönderir, ama ağ dalgalıdır. Sağlayıcının kendi gerçeği, veritabanınızın ise sizin gerçeğiniz vardır. Bunlar tasarlanmazsa sapar.
Sizin açıdan dışarı şu tip mesaj akışıdır: geç, tekrar veya eksik gelebilir:
Bu adımların hiçbiri "tam olarak bir kez" garantilemez. Sadece "belki" garantiler.
Sınırınızın içinde dayanıklı gerçekleri ve dış olayları bunlara bağlamak için gereken minimumu saklayın.
Müşteri ilk siparişi verdiğinde order kaydı pending_payment gibi net bir durumda oluşturun. Ayrıca benzersiz bir sağlayıcı referansı ve müşterinin eylemine bağlı bir idempotency_key ile bir payment_attempt kaydı oluşturun.
İstemci zaman aşımına uğrayıp yeniden denediğinde API ikinci bir sipariş oluşturmak yerine idempotency_keyi arayıp aynı order_id ve mevcut durumu döndürmelidir. Bu tek tercih ağ hatalarında çoğalmayı önler.
Şimdi webhook iki kez gelir. İlk callback payment_attempt'ı authorized olarak günceller ve siparişi paida taşır. İkinci callback aynı handler'a gelir ama sağlayıcı event ID'sini saklayarak veya mevcut durumu kontrol ederek zaten işlendiğini tespit edersiniz ve hiçbir şey yapmazsınız. Yine 200 OK dönebilirsiniz çünkü sonuç zaten doğrudur.
Son olarak, uzlaşma dağınık vakaları halleder. Sipariş bir gecikme sonra hâlâ pending_payment ise, bir arka plan işi saklı referansı kullanarak sağlayıcıyı sorgular. Sağlayıcı "authorized" diyorsa ama webhook'u kaçırdıysanız kayıtları güncellersiniz. Sağlayıcı "failed" diyor ama siz paid olarak işaretlediyseniz inceleme için işaretleyin veya telafi edici eylem (iade) başlatın.
Çoğu çoğaltılmış kayıt ve "takılı" iş akışı, dış sistemde ne olduğuyla (bir istek geldi, bir mesaj alındı) içeride güvenle commit ettiğiniz şeyleri karıştırmaktan kaynaklanır.
Klasik bir hata: istemci "sipariş ver" gönderir, sunucunuz işi başlatır, ağ kopar ve istemci yeniden dener. Her yeniden denemeyi tamamen yeni gerçek olarak ele alırsanız çift ücretlendirme, çift siparişler veya birden fazla e‑posta gönderirsiniz.
Yaygın nedenler:
Bir hatayı daha kötü yapan şey: denetim izi yok. Alanları üzerine yazar ve yalnızca en son durumu saklarsanız, daha sonra uzlaştırmak için gereken kanıtı kaybedersiniz.
İyi bir kontroller sorusu: "Bu handler'ı iki kez çalıştırırsam aynı sonucu alıyor muyum?" Cevap hayırsa, çoğaltmalar nadir bir kenar durum değil, garantilidir.
Hatırlamanız gereken tek şey: uygulamanız mesajlar geç geldiğinde, iki kez geldiğinde veya hiç gelmediğinde bile doğru kalmalıdır.
Bu kontrol listesi, zayıf noktaları çoğaltılmış kayıtlar, eksik güncellemeler veya takılı iş akışlarına dönüşmeden önce bulmanıza yardımcı olur:
Bunlardan birine hızlı cevap veremiyorsanız, genellikle sınırın belirsiz olduğu veya bir durum geçişinin eksik olduğu anlamına gelir.
Pratik sonraki adımlar:
Önce sınırları ve durumları çizin. Her iş akışı için küçük bir durum seti tanımlayın (örneğin: Created, PaymentPending, Paid, FulfillmentPending, Completed, Failed).
En çok riskli yazmalara idempotency ekleyin. En yüksek riskli işlemlerle başlayın: sipariş oluşturma, ödeme alma, iade etme. Idempotency anahtarlarını PostgreSQL'de benzersiz kısıtla saklamak çoğaltmaları güvenli şekilde reddeder.
Uzlaşmayı normal bir özellik olarak ele alın. "Çok uzun süredir bekleyen" kayıtları arayan bir job planlayın, dış sistemleri yeniden kontrol etsin ve yerel durumu onarsın.
Güvenle yineleyin. Geçerli istekleri bilerek yeniden göndererek ve aynı olayı yeniden işleyerek geçişleri ve yeniden deneme kurallarını test edin.
Eğer Koder.ai (koder.ai) gibi sohbet tabanlı bir platformda hızlıca inşa ediyorsanız, bu kuralları üretilen servislere erkenden yerleştirmek yine de faydalıdır: hız otomasyonla gelir, ama güvenilirlik net sınırlar, idempotent handler'lar ve uzlaşmadan gelir.
"Dışarıda" sistemin kontrol etmediği her şeydir: tarayıcılar, mobil ağlar, kuyruklar, üçüncü taraf webhook'lar, yeniden denemeler ve zaman aşımları. Mesajların gecikebileceğini, çoğaltılabileceğini, kaybolabileceğini veya yanlış sırada gelebileceğini varsayın.
"İçeride" ise sizin kontrolünüzde olan şeydir: saklanan durumunuz, uyguladığınız kurallar ve daha sonra kanıtlayabileceğiniz gerçekler (genellikle veritabanınızda).
Çünkü ağ size yalan söyler.
Bir istemcinin zaman aşımına uğraması, sunucunuzun isteği işlememiş olduğu anlamına gelmez. Bir webhook'un iki kez gelmesi, sağlayıcının işlemi iki kez yaptığı anlamına gelmez. Her mesajı "yeni gerçek" olarak ele alırsanız, çift siparişler, çift ücretlendirmeler ve takılı iş akışları üretirsiniz.
Net bir sınır, güvenilmez bir mesajın kalıcı bir gerçek haline geldiği noktadır.
Yaygın sınırlar:
Veri sınırı geçtikten sonra, içeride (örneğin “sipariş yalnızca bir kez ödenebilir” gibi) değişmezlikleri uygulayın.
Idempotency kullanın. Varsayılan kural: aynı niyet birden çok gönderilse bile aynı sonuç üretilmelidir.
Pratik desenler:
Bunu sadece bellekte tutmayın. Koruma garantisinin devam etmesi için idempotency kaydını sınırınız içinde (örneğin PostgreSQL) saklayın.
Saklama kuralı:
Gerçekçi yeniden denemeleri ve gecikmiş callback'leri kapsayacak kadar uzun tutun.
Belirsizliği kabul eden durumlar kullanın.
Basit, pratik bir set:
pending_* (niyeti kabul ettik ama sonucu bilmiyoruz)succeeded / failed (nihai sonucu kaydettik)needs_review (insan veya özel bir iş gerektiren uyuşmazlık tespit edildi)Bu, zaman aşımı sırasında tahmin yapmayı engeller ve uzlaşmayı kolaylaştırır.
Çünkü ağ üzerinden birden fazla sistem arasında atomik taahhüt yapmak genellikle mümkün değildir.
“Siparişi kaydet → kartı ücretlendir → stoğu ayır” gibi senkron iş akışları, bir adım zaman aşımına uğradığında ne yapılacağını belirsiz bırakır. Yeniden denemek çift işleme, denememek ise yarım kalmış iş bırakır.
Bunun yerine niyeti önce saklayın, dış çağrıları ayrı adımlarda yapın ve gerektiğinde telafi edici adımlar uygulayın.
Outbox/inbox deseni ağı mükemmelmiş gibi göstermeden çapraz sistem iletişimini güvenilir hale getirir.
Uzlaşma, kayıtlarınız ile dış bir sistem arasında uyuşmazlık olduğunda nasıl toparlanacağınızdır.
Basit bir yaklaşım:
needs_review olarak işaretleÖdemeler, gönderimler ve abonelikler için uzlaşma zorunludur.
Evet. Hızlı inşa etmek ağı bozmaz—sadece sorunla daha çabuk karşılaşırsınız.
Koder.ai ile servisler üretirken bu varsayımları erkenden katın:
Böylece yeniden denemeler ve çift callback'ler pahalı yerine sıradan hale gelir.