Oyun Kitabı

Ödeme Durum Makinesi Tasarımı: Checkout ve Payment Neden Aynı Şey Değildir? (Odeme Durum Makinesi Tasarimi)

Ödeme Succeeded olması, siparişin Completed olduğu anlamına gelmez. Checkout ve payment yaşam döngülerini ayırmazsanız, üretimde iki gerçek çakışır.

Dağıtık Ödeme Motoru (Distributed Payment Engine)

Bolum 2 / 22

Capture ile complete arasındaki boşluğu kapatan dağıtık ödeme mimarisi serisi.

Distributed payment engine architecture diagram

İki takvim, tek ekran

Müşteri ekranında tek bir “sipariş durumu” görür; ama arka planda en az iki bağımsız durum makinesi çalışır: checkout'un yaşam döngüsü ve payment'ın yaşam döngüsü. Bu iki makineyi tek bir alan (status) üzerinden yönetmeye çalışmak, ilk bölümde bahsettiğimiz “tek gerçek” yanılsamasının durum makinesi versiyonudur.

Checkout:  Init → Processing → FinalizePending → Completed
                                              ↘ Failed / Expired

Payment:   Init → Processing → Captured → Completed
                                       ↘ Failed / Expired

Bu bölüm, bu iki makineyi neden ayrı tasarlamanız ve aralarındaki gecikmeyi neden bir hata değil bir tasarım kararı olarak kabul etmeniz gerektiğini anlatıyor.

Kavramlar ilk geçtiği yerde

📦 Checkout Lifecycle
Siparişin müşteri gözünden geçtiği aşamalar: başlatıldı, işleniyor, tamamlanma bekliyor, tamamlandı.

📦 Payment Lifecycle
Para hareketinin PSP gözünden geçtiği aşamalar: başlatıldı, işleniyor, çekildi (captured), tamamlandı.

📦 Terminal Durum
Geriye dönüşü olmayan, makinenin o dal için sonlandığı durum (Completed, Failed, Expired).

📦 Transition Guard
Bir durumdan diğerine geçişe izin vermeden önce kontrol edilen ön koşul.

📦 State Drift
İki ilişkili durum makinesinin, senkronize olması gereken noktada birbirinden kopması.

Captured, PSP'nin “parayı aldım” demesidir. Completed, sizin “siparişi bitirdim” demenizdir. Bu ikisi aynı olay değildir; aralarında FinalizePending adında bir bekleme durumu vardır ve bu durum saniyeler değil, bazen dakikalar sürebilir.

Neden tek bir status alanı yetmez

Birçok sistemde sipariş tablosunda tek bir status sütunu vardır ve hem “ödeme durumu” hem “sipariş durumu” bu sütuna sıkıştırılır. Bu, iki farklı sorumluluğu tek bir alana sıkıştırmanın klasik belirtisidir: payment webhook'u geldiğinde status'u Completed yapan bir satır, aslında stok rezerve edilmemişken veya fatura kesilmemişken siparişi “bitmiş” gösterir.

Orders
id | status
1  | Completed   ← webhook geldi, ama finalization saga'sı henüz çalışmadı

İki makineyi ayırmak

Doğru model, checkout ve payment'ı ayrı state machine olarak tanımlar ve aralarında yalnızca tek yönlü bir tetikleme ilişkisi kurar: payment'ın Captured durumuna geçişi, checkout'un FinalizePending durumuna geçmesini tetikler; ama checkout'un Completed olması, kendi saga'sının bitmesine bağlıdır.

Payment.Captured  --(tetikler)-->  Checkout.FinalizePending
                                          |
                          stok, finans, bildirim, sepet temizliği tamamlanınca
                                          ↓
                                  Checkout.Completed

Bu ayrım sayesinde “ödeme başarılı ama sipariş hâlâ işleniyor” artık bir hata değil, beklenen ve gösterilebilir bir ara durumdur. Müşteriye “Ödemeniz alındı, siparişiniz hazırlanıyor” demek, sistemin gerçek durumunu doğru yansıtır.

Failed ve Expired'ı kim tetikler

Her iki makinenin de kendi Failed ve Expired dalları vardır ve bunlar birbirinden bağımsız tetiklenebilir. Payment tarafında Expired, PSP'nin belirli bir süre içinde cevap vermemesi anlamına gelir (örneğin 3D Secure onayının tamamlanmaması). Checkout tarafında Expired, finalization saga'sının belirlenen bir süre içinde bitmemesi anlamına gelir — payment Captured olsa bile.

Payment.Captured  +  Checkout finalization 30 dakika içinde bitmedi
        ↓
Checkout.Expired (ama Payment.Captured hâlâ geçerli — para geri iade edilmeli mi, saga retry mi edilmeli, karar operasyonel bir konudur)

Bu senaryo, iki makineyi ayırmanın asıl faydasını gösterir: Payment.Captured durumu bozulmadan, checkout tarafında ayrı bir kurtarma (recovery) süreci başlatabilirsiniz. Tek bir status alanı olsaydı, bu iki gerçeği aynı anda temsil edemezdiniz.

Guard'lar: geçişleri korumak

Her geçişin bir guard'ı olmalıdır. Örneğin Checkout.Processing → Checkout.FinalizePending geçişi, yalnızca ilişkili payment kaydının Captured durumunda olduğu doğrulandıktan sonra gerçekleşmelidir. Guard'sız bir state machine, webhook'ların sırasız gelmesi durumunda (bu seriyle ilgili altıncı bölümde detaylandıracağız) geçersiz durumlara düşebilir.

Guard: Checkout.FinalizePending'e geçiş
  → İlişkili Payment kaydı var mı?
  → Payment.Status == Captured mı?
  → Payment.Amount, Checkout snapshot'ıyla eşleşiyor mu?
  Hepsi doğruysa geçiş serbest; değilse geçiş reddedilir ve olay bir “beklemede” kuyruğuna düşer.

Bu bölümde en çok karışan eşleştirmeler

❌ Payment.Succeeded = Checkout.Completed
✓ Payment.Succeeded, Checkout.FinalizePending'i tetikler; Completed ayrı bir karardır

❌ Tek bir status alanı hem ödeme hem sipariş durumunu taşıyabilir
✓ İki bağımsız yaşam döngüsü, iki bağımsız alan (veya tablo) gerektirir

❌ Failed durumu her zaman “para geri gitti” anlamına gelir
✓ Checkout.Failed, Payment.Captured'ı geçersiz kılmaz; ayrı bir telafi süreci gerekir

❌ Guard'sız bir geçiş, sadece “fazladan kontrol”dür
✓ Guard, sırasız veya tekrarlı olaylara karşı tek savunma hattıdır

Durum makinenizi denetleme listesi

  1. Sipariş tablonuzda ödeme durumu ile sipariş durumu aynı sütunda mı tutuluyor? Ayırın.
  2. Captured ile Completed arasındaki geçişi tetikleyen kod, hangi guard'ları kontrol ediyor?
  3. Payment Captured olduktan sonra checkout saga'sı 30 dakika içinde bitmezse ne oluyor? Bir alarm var mı?
  4. Failed ve Expired durumlarına kimin, hangi olayla geçtiğini net olarak listeleyebiliyor musunuz?
  5. Aynı payment için iki webhook sırasız gelirse, guard bu durumu yakalıyor mu, yoksa geçersiz bir geçişe mi izin veriyor?

Bu beş soruya emin cevabınız yoksa, muhtemelen iki durum makinesini tek bir alanda birleştirmişsinizdir.

Bu bölümden aklında kalması gerekenler

  1. Checkout ve payment, birbiriyle ilişkili ama bağımsız iki durum makinesidir; tek bir status alanına sıkıştırılamaz.
  2. Payment.Succeeded, Checkout.Completed'ı garanti etmez; aralarında ölçülebilir ve gösterilebilir bir FinalizePending aralığı vardır.
  3. Her geçiş bir guard'a bağlanmalıdır; guard'sız geçişler, sırasız veya tekrarlı olaylarda geçersiz durumlara yol açar.
  4. Failed ve Expired, iki makinede bağımsız tetiklenebilir; biri diğerini otomatik geçersiz kılmaz.

Ödeme durumunu ve sipariş durumunu aynı sütuna yazdığınız gün, iki farklı gerçeği tek bir yalana dönüştürmüş olursunuz.

SSS

Sık sorulan sorular

Checkout Lifecycle nedir?

Siparişin müşteri gözünden geçtiği aşamalar: başlatıldı, işleniyor, tamamlanma bekliyor, tamamlandı.

Payment Lifecycle nedir?

Para hareketinin PSP gözünden geçtiği aşamalar: başlatıldı, işleniyor, çekildi (captured), tamamlandı.

"Payment.Succeeded = Checkout.Completed" doğru mu?

Payment.Succeeded, Checkout.FinalizePending'i tetikler; Completed ayrı bir karardır

Bu bölüm neyi sabitler?

Bu bölüm, bu iki makineyi neden ayrı tasarlamanız ve aralarındaki gecikmeyi neden bir hata değil bir tasarım kararı olarak kabul etmeniz gerektiğini anlatıyor. Checkout ve payment, birbiriyle ilişkili ama bağımsız iki durum makinesidir; tek bir `status` alanına sıkıştırılamaz. Müşteri ekranında tek bir “sipariş durumu” görür; ama arka planda en az iki bağımsız durum makinesi çalışır: checkout'un yaşam döngüsü ve payment'ın yaşam döngüsü. Bu iki makineyi tek bir alan (`status`) üzerinden yönetmeye çalışmak, ilk bölümde bahsettiğimiz “tek gerçek” yanılsamasının durum makinesi versiyonudur.

Ogrenilen Muhendislik Prensipleri

  • Checkout ve payment yaşam döngüleri birbiriyle ilişkilidir ama bağımsızdır; aynı alanda temsil edilemez.
  • Payment.Succeeded bir tetikleyicidir, bir sonuç değil; Checkout.Completed kendi saga'sının bitmesine bağlıdır.
  • Guard'sız bir geçiş, sırasız veya tekrarlı bir olayla karşılaştığında geçersiz durumun kapısını açık bırakır.

Okumaya devam et

Okumaya devam et

Seride sonraki yazi

Seride sonraki yazi

Ayni seriden

Paylaş