Fintech'te Go — Staff Engineer Seviyesinde Derinlemesine İnceleme
Go’nun production fintech sistemlerinde gerçekte nasıl kullanıldığı — mimari, idiom’lar, pattern’ler ve gerçekten karşılaşacağınız finansal-domain problemleri.
İçindekiler
- Büyük Resim: Go Fintech’te Nerede Kullanılır
- Referans Mimari
- Fintech Bağlamında Concurrency
- Idiomatic Go
- Go Design Pattern’leri
- Architecture Pattern’leri
- Fintech için Domain-Driven Design
- Payment Service Tasarımı
- Idempotency
- Ledger ve Double-Entry Accounting
- Database Tasarımı
- Transaction Pattern’leri (Outbox, Saga)
- Kafka ve Event-Driven Architecture
- Distributed Systems Temelleri
- Go Concurrency Primitive’leri Özeti
- API Tasarımı
- gRPC
- Security
- Fraud ve Risk
- Reconciliation ve Settlement
- Observability
- Testing
- Production Proje Yapısı
- Go Anti-Pattern’leri
- Performance
- Scalability
- Tam Örnek: Payment & Ledger Platform
- Production-Quality Kod Örneği
- Trade-off Analizi
- Öğrenme Roadmap’i
- Master Checklist
1. Büyük Resim: Go Fintech’te Nerede Kullanılır
Go, fintech’i “hızlı olduğu için” kazanmadı. Belirli bir denge noktasını yakaladığı için kazandı: öngörülebilir performans, I/O-bound servisler için basit concurrency, büyük takımlar için hızlı derleme, küçük memory footprint ve yük altında dev ortamdakiyle aynı şekilde davranan bir runtime. Bu kombinasyon, payment sistemlerinde ham throughput’tan daha çok önem taşır.
Aşağıdaki her domain için: neden Go, ne Go ile yazılır, ne yazılmaz ve gerçek trade-off’lar nelerdir.
Digital Banking (core banking, account servisleri)
- Neden Go: Account servisleri I/O-bound’dur (DB, cache, downstream çağrılar), instance başına yüksek concurrency gerektirir ve sıkıcı/öngörülebilir kalmalıdır. Go’nun goroutine’leri, thread-pool tuning’e gerek kalmadan bir servisin binlerce eşzamanlı account lookup’ını işlemesine izin verir.
- Go ile yazılanlar: account API’leri, balance servisleri, ekstre üretim orkestrasyonu, internal core-banking API’leri.
- Yazılmayanlar: legacy bir bankanın gerçek core banking ledger’ı genellikle COBOL/Java’dır; Go, tam bir core-banking migration’ından önce kenarlarda (BFF, orkestrasyon) kullanılır.
- Trade-off: Go’nun olgun bir ORM ekosisteminin olmaması (bilinçli bir tasarım kararı) daha fazla boilerplate SQL demek — bazı takımlar bunu dezavantaj, bazıları özellik (sihir yok) olarak görür.
Payment Processing / Payment Gateway / Orchestration
- Neden Go: Bu, Go’nun en güçlü uyum sağladığı alan. Yüksek concurrency, düşük latency tail’i (p99, ortalamadan daha önemli), açık hata yönetimi zorunluluğu (burada yutulan bir hata para kaybı demektir) ve küçük, stateless binary’ler olarak kolay deploy.
- Go ile yazılanlar: payment intake API’leri, birden fazla PSP’yi (Stripe, Adyen vb.) çağıran orkestrasyon motorları, retry/idempotency katmanları, webhook alıcıları.
- Alternatif teknoloji: Mevcut Spring ekosistemine sahip Java/Kotlin şirketleri, teknik zorunluluktan değil takım yeteneği yüzünden payment orchestration’ı bazen Kotlin’de tutar.
- Dezavantaj: Go’nun generic’leri genç (1.18’den beri) — Java’nın ağır generic/reflection ile zarif ifade edebildiği bazı orchestration DSL’leri Go’da daha ayrıntılı olur.
Card Processing
- Neden Go: Aşırı yüksek throughput, çok sıkı latency SLA’ları (issuer/network authorization pencereleri milisaniyeler seviyesinde), deterministik GC duraklamaları önemli. Go’nun düşük-latency GC’si (Go 1.8+‘dan beri sub-milisaniye pause hedefleri) authorization-path servislerine iyi uyar.
- Uyarı: En sıcak yollar (HSM etkileşimi, çok yüksek hacimde ISO 8583 mesaj işleme) bazen daha da sıkı tail latency için C/C++ veya GraalVM native-image ile Java’dır; Go burada son derece rekabetçidir ama evrensel olarak “tek seçenek” değildir.
Bank Transfer / Money Movement / Wallet
- Go çok doğal bir uyum sağlar: çoğunlukla CRUD + orkestrasyon + external network çağrıları + katı consistency gereksinimleri. Wallet balance servisleri yaygın olarak Go + PostgreSQL ve row-level locking ile yazılır.
Ledger Sistemleri
- Neden Go: Ledger’lar önce doğruluk, sonra performans ister. Go’nun statik tiplemesi, açık hata yönetimi ve gizli kontrol akışının olmaması (exception yok), ledger mantığının akıl yürütülmesini ve code review’unu kolaylaştırır — bir bug’ın para yaratması veya yok etmesi anlamına geldiği durumlarda kritik.
- Dezavantaj: Yerleşik bir decimal tipi yok; integer minor unit’ler veya
shopspring/decimalgibi kütüphaneler dikkatle kullanılmalı, çünküfloat64para için kabul edilemez.
Trading / Brokerage
- Neden Go (kısmen): order-management sistemleri, market-data dağıtımı ve risk-check servisleri Go’nun concurrency’sinden faydalanır.
- Go’nun kaybettiği yer: gerçek matching engine / ultra-low-latency yolu genellikle C++‘dır (veya özel çözümler — kernel-bypass networking, custom allocator’lar) çünkü Go’nun GC’si, ne kadar ayarlanırsa ayarlansın, sub-mikrosaniye matching engine’ler için yeterince deterministik değildir. Go, bunun bir katman üstünde yaygındır.
Crypto / Blockchain Altyapısı
- Go burada yapısal olarak baskındır (Ethereum’un
go-ethereum‘u, Cosmos SDK’nın büyük kısmı, Hyperledger Fabric) çünkü blockchain node yazılımı eşzamanlıdır, network-ağırlıklıdır ve binlerce node operatörüne gönderebileceğiniz tek bir statik binary’den fayda sağlar.
Fraud Detection / Risk Engine
- Neden Go: rules-engine ve orkestrasyon katmanı (birden fazla scoring servisini çağırma, sinyalleri toplama, timeout’ları zorlama) Go için harika bir uyum.
- Python’un kazandığı yer: gerçek ML model eğitimi ve genellikle model serving (Python-native framework’ler kullanılıyorsa) Python’da kalır; Go sıklıkla bir Python/ONNX/TF-serving modelini gRPC çağrısı arkasında sarmalar.
KYC / AML
- Orkestrasyon ve döküman/API entegrasyon katmanı için Go (identity verification sağlayıcılarını çağırma, sanctions list tarama API’leri). Kurallar Go’da olabilir; ağır döküman OCR/ML Python-tabanlı servislerde kalır.
Reconciliation / Settlement
- Batch odaklı ama zaman sınırlı; Go’nun hızlı başlangıcı ve düşük memory footprint’i, bir settlement penceresi içinde bitmesi gereken zamanlanmış batch job’lar için iyidir; ancak çok büyük batch/ETL-tarzı reconciliation için veri hacmi nedeniyle bazen Spark/Flink (JVM) kazanır.
Financial Data Processing / Event Processing / Real-Time Sistemler
- Go’nun concurrency modeli, servis seviyesinde stream processing için mükemmel bir eşleşmedir (devasa dağıtık stream-processing framework seviyesinde değil — bu Flink/Kafka Streams/JVM’in alanı, ama Go client’lar Kafka pipeline’larına yoğun şekilde katılır).
API Gateway / Microservices / Internal Altyapı
- Go genel olarak infra tooling’de baskındır (Kubernetes, Docker, Terraform, Consul, Envoy’un control plane’i), bu yüzden fintech şirketleri kendi gateway/internal servislerini Go’da standartlaştırarak güçlü internal tooling/kütüphane yeniden kullanımı elde eder.
Notification Sistemleri
- Basit, yüksek-throughput, stateless — ders kitabı Go microservice örneği.
Özet tablo:
| Domain | Go uyumu | Yaygın alternatif |
|---|---|---|
| Payment orchestration | Mükemmel | Kotlin/Java (takım legacy’si) |
| Card auth hot path | Çok iyi | C++/Java-GraalVM (aşırı tail latency) |
| Ledger | Mükemmel | Java (büyük bankalar), Rust (bazı yeni oyuncular) |
| Matching engine | Bir katman üstünde iyi | C++ (engine’in kendisi) |
| Fraud kural orkestrasyonu | Mükemmel | — |
| ML scoring/eğitim | Zayıf uyum | Python |
| Blockchain node yazılımı | Mükemmel (baskın) | Rust (bazı yeni chain’ler) |
| Batch reconciliation (çok büyük hacim) | İyi | Çok büyük ETL için Spark/Flink |
| Internal infra/gateway | Mükemmel | — |
2. Referans Mimari
┌─────────────┐
│ Client │
└──────┬──────┘
│ HTTPS
┌──────▼──────┐
│ API Gateway │ (authN edge, rate limiting, TLS termination)
└──────┬──────┘
│
┌──────▼──────┐
│ Auth │ (OAuth2/OIDC token doğrulama)
└──────┬──────┘
│
┌──────▼──────┐
│ Payment │
│ Service │
└──┬───┬───┬──┘
┌────────────┘ │ └────────────┐
┌────────▼──────┐ ┌───────▼──────┐ ┌────────▼───────┐
│ Account Service│ │Ledger Service│ │ Fraud Service │
└────────────────┘ └──────┬───────┘ └────────────────┘
│
┌──────▼───────┐
│ Risk Service │
└──────┬───────┘
│
┌───────▼────────┐
│ Payment Provider│ (external PSP / kart ağı)
└───────┬─────────┘
│
┌─────────────┼─────────────┐
┌───────▼──────┐ ┌────▼─────┐ ┌─────▼──────────┐
│ Notification │ │Reconcile │ │ Kafka / Event │
│ Service │ │ Service │ │ Bus │
└──────────────┘ └──────────┘ └───────┬─────────┘
│
┌───────────────────┼───────────────────┐
┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐
│PostgreSQL │ │ Redis │ │Object Storage│
└───────────┘ └─────────────┘ └─────────────┘
Component analizi
API Gateway
- Sorumluluk: TLS termination, kaba-taneli rate limiting, request routing, request ID enjeksiyonu.
- Neden Go: tek statik binary, çok düşük request başına overhead, iyi ekosistem (sidecar olarak deploy edilen Envoy/Kong, veya bir service mesh önünde ince bir Go gateway).
- Failure senaryosu: gateway çökmesi → load balancer arkasında birden çok replica kullanın; burada asla state tutmayın.
- Scaling: tamamen yatay, stateless.
- Observability: request ID’li access log’lar, route başına latency histogram’ları.
- Security: sadece TLS 1.2+/1.3, downstream servislere mTLS, WAF kuralları.
Payment Service
- Sorumluluk: payment lifecycle’ını orkestre eder: validate → idempotency check → risk check → provider çağrısı → ledger yazımı → event publish.
- Neden Go: en yüksek-concurrency, en latency-hassas orkestrasyon noktası; goroutine’ler sınırlı timeout’larla fraud/risk/provider çağrılarına eşzamanlı olarak dağılmayı sağlar.
- API:
POST /payments,GET /payments/{id},POST /payments/{id}/refund. - Database: Postgres —
payments,idempotency_keystabloları. - Yayınlanan event’ler:
payment.created,payment.completed,payment.failed. - Failure senaryoları: sonucu belirsiz provider timeout’u (bkz. §8), DB yazımı başarılı ama event publish başarısız (bkz. §12, Outbox).
- Scaling: stateless yatay ölçekleme; asıl darboğaz servis değil, idempotency tablosu ve DB’dir.
- Observability: fraud → provider → ledger boyunca uzanan distributed trace; tek bir correlation ID.
- Security: servisler arası mTLS, PCI-scope izolasyonu (payment servisi raw PAN’a dokunmamalı — bu upstream’de tokenize edilir veya PCI-scoped bir vault tarafından yönetilir).
Account Service
- Sorumluluk: customer/account entity’lerinin, balance’ların (ideal olarak ledger’dan türetilen bir read model) sahibi.
- Database: Postgres, balance check’leri için güçlü tutarlı okumalar.
- Scaling: okuma-ağırlıklı balance-check trafiği için read replica’lar; yazmalar ledger üzerinden geçer.
Ledger Service
- Sorumluluk: para hareketinin tek doğruluk kaynağı — double-entry, immutable, append-only.
- Neden Go: doğruluk-kritik; Go’nun açık hata yönetimi ve exception’ların olmaması her hata yolunu code review’da görünür kılar.
- Database:
SERIALIZABLEveya dikkatli row-locking ile Postgres (bkz. §10, §11). - Failure senaryosu: asla dengesiz bir entry (debit ≠ credit) kabul etmemeli — bunu sadece uygulama mantığıyla değil, bir DB constraint’i veya transactional check ile zorunlu kılın.
- Scaling: consistency gereksinimleri nedeniyle genellikle yatay olarak ölçeklendirilmesi en zor servistir; account ID’ye göre partition/shard edin.
Fraud Service / Risk Service
- Sorumluluk: payment yolunda gerçek zamanlı scoring, genellikle sıkı bir zaman bütçesiyle (ör. 150ms), bu süre aşılırsa “skor mevcut değil, varsayılan risk duruşunu uygula” fallback’i ile.
- Neden Go: birden fazla sinyal kaynağına eşzamanlı olarak dağılmalı ve katı bir deadline’ı zorlamalı —
context.WithTimeoutbunun için ders kitabı aracı.
Payment Provider (entegrasyon katmanı)
- Sorumluluk: external PSP’ler/kart ağlarına adapter; provider başına retry/circuit-breaker mantığının sahibi.
- Failure senaryosu: provider, timeout’unuzdan önce hiçbir şey döndürmezse — transaction durumu başarısız değil, bilinmez‘dir. Bu açıkça ele alınmalıdır (bkz. §8, §9).
Notification Service
- Stateless, Kafka’dan event tüketir, email/SMS/push sağlayıcılarına dağıtır. Basit, yüksek-concurrency, ders kitabı worker-pool kullanım örneği.
Reconciliation Service
- Internal ledger durumunu provider ekstreleriyle karşılaştıran zamanlanmış batch job; discrepancy raporları ve düzeltme kayıtları üretir (bkz. §20).
3. Fintech Bağlamında Concurrency
Go’da concurrency, payment sistemlerinde bir performans lüksü değil — bir request içinde birden fazla downstream servisi (fraud, risk, provider) çağırırken latency SLA’larını nasıl karşıladığınızın ta kendisidir.
Pattern: Fraud + Risk check’leri için sınırlı timeout ile fan-out
func (s *PaymentService) evaluate(ctx context.Context, p Payment) (RiskResult, error) {
ctx, cancel := context.WithTimeout(ctx, 150*time.Millisecond)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
var fraudScore, riskScore int
g.Go(func() error {
score, err := s.fraudClient.Score(ctx, p)
if err != nil {
return fmt.Errorf("fraud score: %w", err)
}
fraudScore = score
return nil
})
g.Go(func() error {
score, err := s.riskClient.Score(ctx, p)
if err != nil {
return fmt.Errorf("risk score: %w", err)
}
riskScore = score
return nil
})
if err := g.Wait(); err != nil {
// Fallback: ödemeyi doğrudan başarısız yapma — muhafazakar bir varsayılan uygula.
return RiskResult{Score: DefaultConservativeScore, Degraded: true}, nil
}
return RiskResult{Score: combine(fraudScore, riskScore)}, nil
}
errgroup, fan-out’u ilk-hata propagasyonu ve otomatik context cancellation ile birlikte verir — risk zaten başarısız olduktan sonra hâlâ fraud çağrısını bekleyen bir goroutine’i sızdırmamanız için kritik.
Reconciliation / notification fan-out için worker pool
func processInParallel(ctx context.Context, items []Item, workers int, fn func(context.Context, Item) error) error {
sem := make(chan struct{}, workers) // sınırlı concurrency
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item
select {
case sem <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
g.Go(func() error {
defer func() { <-sem }()
return fn(ctx, item)
})
}
return g.Wait()
}
Sınırsız goroutine oluşturma (for _, item := range items { go process(item) } semaphore olmadan) klasik bir hatadır: binlerce eşzamanlı DB bağlantısı veya provider çağrısı açabilir ve bir dependency’i çökertebilir.
Dikkatsiz concurrency’de neler ters gider
- Race condition: iki goroutine, memory’deki bir balance cache’ini mutex olmadan okuyup-değiştirip-yazıyor → kayıp güncellemeler. Fintech etkisi: para, cache’lenmiş bir toplamdan sessizce kaybolur.
- Deadlock: goroutine A, lock 2’yi bekleyerek lock 1’i tutuyor; goroutine B, lock 1’i bekleyerek lock 2’yi tutuyor — payment transfer’inde tutarlı bir lock sırası olmadan birden fazla account satırı kilitlerken klasik bir durum (her zaman deterministik bir sırada kilitleyin, ör. account ID artan sırayla).
- Goroutine leak: hiç yazılmayan ve
ctx.Done()üzerinde select yapmayan bir channel’dan okuyan bir goroutine başlatmak. Zamanla bu memory’yi tüketir. Bir payment retry-loop’unda bu çok yaygın bir bug’dır. - Duplicate transaction: bir client, timeout sonrası bir POST /payments’i tekrar dener ve servis — idempotency key kontrolü olmadan — bunu iki kez işler. Bu katı bir concurrency bug’ı değil, bir business logic bug’ıdır — ama concurrency (neredeyse aynı anda gelen iki request), DB seviyesinde zorunlu kılınmazsa idempotency-key işlemindeki race’i tetikleyen şeydir (bkz. §9).
- Inconsistent state: ledger yazımı başarılı olur, ama Kafka event’ini publish eden goroutine commit’ten önce panic atar — payment artık ödenmiştir ama sistemin geri kalanına hiç haber verilmemiştir (bkz. §12, Outbox).
4. Idiomatic Go
Her idiom için: ne, neden, ne zaman kullanılmalı/kullanılmamalı, fintech örneği, kod.
Composition over inheritance (Kalıtım yerine kompozisyon)
- Ne: Go’da kalıtım yoktur; davranışı embedding ve interface’ler aracılığıyla oluşturursunuz.
- Neden: kırılgan base-class problemlerinden kaçınır; her dependency açıktır.
- Ne zaman kullanılmamalı: kendinizi “belki gerekir diye” 4+ tip embed ederken buluyorsanız, muhtemelen yanlış bir abstraction’ı modelliyorsunuzdur.
- Fintech örneği:
type BaseHandler struct {
Logger *slog.Logger
}
type PaymentHandler struct {
BaseHandler
svc PaymentService
}
Küçük interface’ler
- Ne: Go interface’lerinin 1-3 method ile sınırlı tutulması en iyisidir (
io.Readerklasik örnektir). - Neden: küçük interface’ler kolayca mock’lanabilir ve compose edilebilir.
- Fintech örneği:
type PaymentProvider interface {
Charge(ctx context.Context, req ChargeRequest) (ChargeResult, error)
}
Charge, Refund, Void, Capture, GetStatus… gibi 15 method’lu bir PSPClient interface’i değil — bunun yerine sorumluluğa göre bölün.
Interface kabul et, concrete tip döndür
- Ne: fonksiyon parametreleri interface olmalı (test edilebilirlik için); dönüş değerleri concrete struct olmalı (netlik için ve caller’ları ihtiyaç duymadıkları bir interface’e zorlamamak için).
func NewLedgerService(db *sql.DB, publisher EventPublisher) *LedgerService { ... }
Implicit interface’ler
- Ne: Go interface’leri yapısal olarak tatmin edilir,
implementsanahtar kelimesi yoktur. - Fintech kod tabanlarında neden önemli: kullanım noktasında dar bir interface tanımlayabilirsiniz (ör.
paymentpaketi içindeLedgerService‘ten ihtiyacınız olan tam olarak iki method’u tanımlayın), paketlerin birbirinin interface’inden haberdar olmasına gerek kalmadan paketleri ayırır.
Constructor injection
func NewPaymentService(
repo PaymentRepository,
ledger LedgerClient,
fraud FraudClient,
publisher EventPublisher,
) *PaymentService {
return &PaymentService{repo: repo, ledger: ledger, fraud: fraud, publisher: publisher}
}
DI framework’e gerek yok — Go takımları genellikle, kolayca unit test edilebilen açık constructor’lar lehine sihirli reflection-tabanlı DI container’lardan kaçınır.
Functional options
type ProviderOption func(*ProviderClient)
func WithTimeout(d time.Duration) ProviderOption {
return func(c *ProviderClient) { c.timeout = d }
}
func WithRetries(n int) ProviderOption {
return func(c *ProviderClient) { c.retries = n }
}
func NewProviderClient(baseURL string, opts ...ProviderOption) *ProviderClient {
c := &ProviderClient{baseURL: baseURL, timeout: 5 * time.Second, retries: 2}
for _, opt := range opts {
opt(c)
}
return c
}
Çoğu caller’ın varsayılanları istediği ama birkaçının override etmesi gerektiği provider/HTTP client’lar için sürekli kullanılır (ör. daha yavaş bir provider’ın daha uzun bir timeout’a ihtiyacı vardır).
Açık hata yönetimi / wrapping
result, err := s.ledger.Post(ctx, entry)
if err != nil {
return fmt.Errorf("posting ledger entry for payment %s: %w", p.ID, err)
}
Her hata yolu görünürdür — gizli bir throws yoktur. Fintech code review’unda, bir reviewer’ın “ledger hatasını yutup yine de devam ediyorsun” demesini sağlayan şey budur.
errors.Is / errors.As / sentinel error’lar / custom error’lar
var ErrInsufficientFunds = errors.New("insufficient funds")
type ProviderError struct {
Code string
Message string
}
func (e *ProviderError) Error() string { return fmt.Sprintf("provider error %s: %s", e.Code, e.Message) }
// caller:
if errors.Is(err, ErrInsufficientFunds) {
return http.StatusUnprocessableEntity
}
var pErr *ProviderError
if errors.As(err, &pErr) && pErr.Code == "timeout" {
// retry mantığı
}
Context propagation
- Ne:
context.Context, cancellation, deadline ve request-scoped değerleri (trace ID) her çağrı boyunca taşır. - Fintech kuralı: I/O yapan (DB, HTTP, Kafka) her fonksiyon ilk parametre olarak
ctx context.Contextalmalıdır, tam durak.
defer
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback() // commit edilmişse no-op; herhangi bir yol erken dönerse güvenlik ağı
...
return tx.Commit()
Zero-value prensibi
- Ne: tiplerinizi zero value’ları yararlı olacak şekilde tasarlayın.
var buf bytes.Bufferhemen çalışır. - Fintech örneği:
var m Money, muhtemelen verilen para biriminde0olarak varsayılan olmalı, panic atmamalı — ama dikkat: para kodunda zero-value birCurrency("") genellikle tehlikelidir, bu yüzden finansal tipler genellikle bilinçli olarak zero-value idiom’unu bozar ve constructor gerektirir (NewMoney(amount, currency)), nedenini de dokümante eder.
Pointer vs value receiver’lar
- Receiver’ı mutate ediyorsa veya struct büyükse pointer receiver kullanın; küçük, immutable tipler için (bir
Moneyvalue object gibi) copy-safety elde etmek için value receiver kullanın.
Package visibility / internal/ paketleri
internal/ledger,internal‘ın kök aldığı module tree’nin dışında import edilemez — Go’nun “bu bir implementasyon detayıdır"ı ayrı repo’lara veya bytecode-seviyesi erişim kontrolüne gerek kalmadan zorunlu kılma şekli budur.
Domain’e göre paketleme (katmana göre değil)
- Fintech takımları
internal/handlers,internal/services,internal/repositoriesyerineinternal/payment,internal/ledger,internal/account‘ı güçlü şekilde tercih eder — tam argüman için §23’e bakın.
Generics
func MapSlice[T, U any](in []T, fn func(T) U) []U {
out := make([]U, len(in))
for i, v := range in {
out[i] = fn(v)
}
return out
}
Generic collection helper’ları ve type-safe repository’ler için yararlıdır; insanlar Java-tarzı generic abstraction hiyerarşileri kurmaya çalıştığında aşırı kullanılır — Go kültürü hâlâ yanlış abstraction yerine tekrarı tercih eder.
Standard-library-first
- Fintech Go takımları, framework’lere yönelmeden önce
net/http,database/sql,encoding/json,context,crypto/*‘a ağırlıklı olarak dayanır — PCI-scoped bir kod tabanı için denetlenecek daha az dependency, sadece bir stil tercihi değil gerçek bir security faydasıdır.
5. Go Design Pattern’leri
Go pattern’leri 1:1 çevrilmiş GoF pattern’leri değildir — birçok GoF pattern’i Go’da “bir fonksiyon kullan” veya “bir interface kullan"a indirgenir.
Java Strategy Pattern → Go fonksiyon tipi
Java Interface hiyerarşisi → Go küçük interface + kompozisyon
Java kalıtım → Go kompozisyon / embedding
Creational (Yaratımsal)
Factory
- Problem: config’e göre farklı
PaymentProviderimplementasyonları oluşturmak gerekiyor. - Go implementasyonu:
func NewProvider(kind string, cfg Config) (PaymentProvider, error) {
switch kind {
case "stripe":
return stripe.New(cfg), nil
case "adyen":
return adyen.New(cfg), nil
default:
return nil, fmt.Errorf("unknown provider: %s", kind)
}
}
- Fintech use case: merchant konfigürasyonu/bölgesine göre provider seçimi.
- Avantajlar: construction mantığını merkezileştirir.
- Dezavantajlar: büyük bir switch büyür; çok sayıda provider için bir registry map düşünün.
- Alternatif: her provider paketi için
init()ile doldurulan birmap[string]func(Config) PaymentProviderregistry.
Builder
- Go’da klasik Builder nadiren kullanılır — config-ağırlıklı construction için functional options (§4) neredeyse tamamen onun yerini alır.
Functional Options — bkz. §4.
Singleton / sync.Once
var (
dbOnce sync.Once
dbConn *sql.DB
)
func GetDB() *sql.DB {
dbOnce.Do(func() {
dbConn, _ = sql.Open("postgres", dsn)
})
return dbConn
}
- Fintech uyarısı: global singleton’lar test etmeyi zorlaştırır —
*sql.DB‘yi constructor injection ile açıkça geçirmeyi tercih edin;sync.Oncetam bir DB singleton’ından çok tek seferlik initialization için (ör. bir config yüklemek veya bir regex derlemek) kullanılır.
Structural (Yapısal)
Adapter
- Problem: internal
PaymentProviderinterface’iniz belirli bir PSP SDK’sının şekliyle uyuşmuyor.
type stripeAdapter struct{ client *stripe.Client }
func (a *stripeAdapter) Charge(ctx context.Context, req ChargeRequest) (ChargeResult, error) {
params := toStripeParams(req)
charge, err := a.client.Charges.New(params)
if err != nil {
return ChargeResult{}, translateStripeErr(err)
}
return fromStripeCharge(charge), nil
}
- Fintech use case: her PSP entegrasyonu kendi
PaymentProviderinterface’iniz etrafında bir Adapter’dır.
Decorator
func WithRetry(p PaymentProvider, attempts int) PaymentProvider {
return &retryingProvider{inner: p, attempts: attempts}
}
func WithMetrics(p PaymentProvider) PaymentProvider {
return &meteredProvider{inner: p}
}
provider := WithMetrics(WithRetry(stripeAdapter, 3))
- Fintech use case: adapter’ı değiştirmeden herhangi bir
PaymentProvider‘a retry, circuit breaker ve metrics katmanları eklemek.
Facade
- validate → fraud → provider → ledger orkestrasyonunu tek bir method arkasında gizleyen bir
PaymentFacade, HTTP handler tarafından kullanılır. Handler’ları ince tutar.
Proxy
- Yavaş bir “merchant config” lookup servisinin önünde caching proxy; aynı interface’i implement eder, delegate etmeden önce bir cache kontrolü ekler.
Behavioral (Davranışsal)
Strategy
type FeeStrategy func(amount Money) Money
func PercentageFee(pct float64) FeeStrategy {
return func(amount Money) Money { return amount.Mul(pct) }
}
func FlatFee(fee Money) FeeStrategy {
return func(amount Money) Money { return fee }
}
- Fintech use case: merchant tier’a göre farklı fee hesaplama strateji’leri — bu kadar basit bir şey için sadece bir fonksiyon tipi,
interface{ Calculate() }boilerplate’ine gerek yok (strateji daha fazla state/method gerektiriyorsa bir interface de işe yarar).
State
type PaymentState string
const (
StatePending PaymentState = "PENDING"
StateProcessing PaymentState = "PROCESSING"
StateCompleted PaymentState = "COMPLETED"
StateFailed PaymentState = "FAILED"
)
var validTransitions = map[PaymentState][]PaymentState{
StatePending: {StateProcessing, StateFailed},
StateProcessing: {StateCompleted, StateFailed},
}
func (p *Payment) TransitionTo(next PaymentState) error {
for _, allowed := range validTransitions[p.State] {
if allowed == next {
p.State = next
return nil
}
}
return fmt.Errorf("invalid transition %s -> %s", p.State, next)
}
- Fintech use case: payment/settlement lifecycle state machine’leri — bu pattern tek başına, geçersiz geçişleri compile-time-kontrollü bir veri yapısı haline getirerek “payment sonsuza kadar PROCESSING’de takılı kaldı” bug sınıfını büyük ölçüde önler.
Command
- “Bu payment’ı geri al” veya “bu düzeltme kaydını uygula"yı
Execute(ctx) errorimplement eden bir struct olarak kapsüllemek — auditable, kuyruklanabilir bir command log için yararlı (event sourcing ile iyi çalışır, §6).
Chain of Responsibility
type Middleware func(http.Handler) http.Handler
handler = LoggingMiddleware(AuthMiddleware(RateLimitMiddleware(paymentHandler)))
- Fintech use case: HTTP middleware zincirleri (auth, rate limiting, idempotency check, logging) — bu, Go fintech kodunda en yaygın CoR kullanımıdır.
Observer
- Go, process-içi klasik Observer’ı nadiren implement eder; bunun yerine Kafka, sistem seviyesinde Observer pattern’idir — servisler domain event’leri yayınlar, ilgilenen servisler abone olur. Process içinde, gerektiğinde basit bir callback fonksiyonları dizisi veya channel’lar yeterlidir.
6. Architecture Pattern’leri
Layered / Clean / Hexagonal / Onion
Bunların hepsi aynı temel fikri paylaşır: domain mantığı infrastructure’a bağımlı olmamalı. Go’da bu genellikle Java’ya göre çok daha ucuza sağlanır:
// domain katmanı ihtiyaç duyduğu interface'i tanımlar
type LedgerRepository interface {
Post(ctx context.Context, entry LedgerEntry) error
}
// infrastructure katmanı onu implement eder
type postgresLedgerRepo struct{ db *sql.DB }
func (r *postgresLedgerRepo) Post(ctx context.Context, e LedgerEntry) error { ... }
Bu, Hexagonal/Clean Architecture’ın “dependency inversion"udur — framework yok, annotation yok, sadece consumer tarafından tanımlanan bir interface.
Clean/Hexagonal Architecture Go’da gerekli mi, yoksa overengineering mi?
Dürüst cevap: büyük ölçüde proje büyüklüğüne bağlı, ve Go kültürü varsayılan olarak ağır katmanlamaya karşı direnç gösterme eğilimindedir.
- Küçük startup (1-5 mühendis, tek bir payment flow): Port/adapter/use-case katmanlarına sahip tam Clean Architecture genellikle abartıdır. Bir service struct’ı, bir repository interface’i ve bir Postgres implementasyonu içeren
internal/paymentpaketi yeterlidir. 3 kişilik bir takım içinusecase/,entity/,interface_adapter/katmanları eklemek gerçek bir fayda olmadan sizi yavaşlatır. - Orta ölçekli fintech (birkaç domain, 20-80 mühendis): ince bir interface sınırıyla (repository interface’leri, provider interface’leri) domain’e göre paketleme en tatlı nokta — derin bir katman pastası olmadan test edilebilirlik ve değiştirilebilirlik elde edersiniz.
- Büyük fintech (yüzlerce mühendis, birçok bounded context): domain sınırları, bir domain içindeki katmanlamadan daha önemlidir. Yatırım, her servis içinde derin Clean Architecture katmanlamasına değil, net servis sınırlarına (DDD bounded context’leri, §7) gitmelidir.
- Banka-ölçeği sistem (regülasyonlu, on yıllar süren kod tabanı): burada Hexagonal Architecture disiplini kendi maliyetini karşılar — DB ve PSP entegrasyonu, 10+ yıllık bir sistem ömrü boyunca gerçekten değiştirilir, ve izolasyon kendi kendini finanse eder. Ama burada bile Go takımları, tam Java-tarzı katman taksonomisi yerine “yeterince” ports-and-adapters implement etme eğilimindedir.
Pratik kural: repository/provider interface’lerini tanımlayın, business mantığını doğrudan sql.DB ve http.Client tiplerinden arındırın — bu, Hexagonal Architecture’ın değerinin %80’idir. Takım büyüklüğünüz ve sistem ömrünüz haklı çıkarmadıkça her katmanı isimlendirme törenini atlayın.
DDD, Modular Monolith, Microservices, EDA, CQRS, Event Sourcing, SOA
Ayrıntılı olarak §7 (DDD) ve §12-13’te (event pattern’leri) ele alınmıştır. Hızlı konumlandırma:
- Modular Monolith: çoğu fintech startup’ı burada başlamalı — bir deployable, katı internal paket sınırları (
internal/payment,internal/ledger‘ın private tiplerini import edemez), sadece bir takım veya scaling sınırı bunu gerektirdiğinde microservice’lere bölünmelidir. - Microservices: bağımsız scaling ihtiyaçlarınız (fraud scoring, ledger yazımlarından çok farklı ölçeklenir) veya bağımsız takım sahipliği olduğunda haklıdır — sadece “modern mimari böyle görünüyor” diye haklı değildir.
- CQRS: ledger sistemleri için çok yaygındır — yazmalar katı double-entry validasyonundan geçer, okumalar denormalize edilmiş bir balance projection’ından servis edilir (async veya aynı transaction içinde güncellenir).
- Event Sourcing: özellikle ledger’lar için kullanılır (ledger doğası gereği zaten bir event log’dur — bkz. §10) ama nadiren her domain’e uygulanır; ES’i, diyelim ki bir merchant-profile CRUD servisine uygulamak genellikle overengineering’dir.
7. Fintech için Domain-Driven Design
Fintech örnekleriyle yapı taşları
- Entity (kimliği var, zamanla değişebilir):
Account,Payment,Merchant. - Value Object (immutable, öznitelikleriyle tanımlanır):
Money{Amount, Currency},CardNumber(tokenize edilmiş),Address.
type Money struct {
amountMinorUnits int64 // asla float64 değil
currency string
}
func NewMoney(minorUnits int64, currency string) Money {
return Money{amountMinorUnits: minorUnits, currency: currency}
}
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, fmt.Errorf("currency mismatch: %s vs %s", m.currency, other.currency)
}
return Money{m.amountMinorUnits + other.amountMinorUnits, m.currency}, nil
}
- Aggregate / Aggregate Root:
Payment,PaymentAttemptentity’lerini veMoneyvalue object’lerini içeren bir aggregate root’tur; tüm yazmalar invariant’ları zorlamak için root üzerinden geçer (ör. toplam iade edilen ≤ toplam tahsil edilen). - Repository:
PaymentRepositoryinterface’i — domain tarafından sahip olunan, infrastructure tarafından implement edilen persistence abstraction’ı. - Domain Service: tek bir entity’e ait olmayan stateless mantık, ör. hem
MerchanthemPayment‘a ihtiyaç duyanFeeCalculationService. - Application Service: bir use case’i orkestre eder (
ProcessPaymentUseCase) — domain servislerini, repository’leri çağırır, event’leri yayınlar; HTTP handler’ınızın çağırdığı şey budur. - Domain Event:
PaymentCompleted,PaymentFailed— aggregate tarafından tetiklenir, transaction commit olduktan sonra yayınlanır (bkz. Outbox, §12). - Integration Event: bir domain event’inin, Kafka topic’i üzerinden diğer bounded context’lere geçerken external, versiyonlanmış, geriye-uyumlu şekli — kendi context’i içinde serbestçe değişebilen internal domain event’inden bilinçli olarak ayrı bir kavram.
Bounded context’ler
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Payment Context │ │ Ledger Context │ │ Fraud Context │
│ │ │ │ │ │
│ Payment │ │ Account (ledger) │ │ Transaction Risk│
│ PaymentAttempt │ │ Entry │ │ Score │
│ Refund │ │ Balance │ │ Rule │
└────────┬─────────┘ └─────────┬─────────┘ └────────┬────────┘
│ integration event'leri │ │
└───────────►Kafka◄─────┴───────────►Kafka◄─────┘
Kritik olarak: Ledger context’indeki “Account”, Customer/Account-service context’indeki “Account” ile aynı model değildir. Ledger context’i bir account’u sadece debit/credit’lerin ve bir balance’ın hedefi olarak önemser; Account-service context’i KYC durumu, sahiplik, limitler vb. ile ilgilenir. Bunları context’ler arasında tek bir paylaşılan Account struct’ı olarak modellemek, fintech takımlarının yaptığı #1 DDD hatasıdır — tamamen farklı sebeplerle değişen iki şey arasında yanlış bir bağlantı yaratır (bir compliance kuralı değişikliği bir ledger migration’ı gerektirmemelidir).
8. Payment Service Tasarımı
POST /payments
│
▼
1. Validate (schema, currency, amount > 0)
│
▼
2. Idempotency check (Idempotency-Key header)
│
▼
3. Payment oluştur (status=PENDING, DB transaction içinde)
│
▼
4. Risk/Fraud check (sınırlı timeout, hata durumunda fallback)
│
▼
5. Payment Provider çağrısı (çağrıdan önce status=PROCESSING)
│
▼
6. Ledger yazımı (sadece provider onayladıktan sonra)
│
▼
7. Event publish (Outbox üzerinden — bkz. §12)
│
▼
8. Response
Zor problemler, tek tek
Duplicate request — client, network aksaklığından sonra tekrar dener. Sadece memory-içi bir kontrol değil, DB katmanında bir unique constraint ile zorlanan idempotency key’ler ile çözülür (§9).
Network timeout (client → servisiniz) — client, isteğinizi alıp almadığınızı bilmiyor. Idempotency key’lerin neden client tarafından üretilmesi ve retry’da gönderilmesi gerektiği tam olarak bu — sunucu tarafındaki timeout handling’iniz ayrı bir konudur.
Provider timeout (servisiniz → PSP) — provider‘ın charge’ı işleyip işlemediğini bilmiyorsunuz. Bu, payment sistemlerindeki en zor tek problemdir. Payment PROCESSING / AWAITING_PROVIDER_CONFIRMATION durumuna geçmeli, asla körlemesine sessizce tekrar denenmemeli ve şunlarla reconcile edilmelidir:
- Bir provider status-check API çağrısı (provider, idempotency key’inize göre sorgulamayı destekliyorsa
GET /charges/{id}), veya - Provider’ın webhook’unu beklemek, veya
- Bir eşiğin ötesinde
PROCESSING‘de kalan her şeyi yakalayan bir reconciliation job’u (§20).
Provider başarılı ama client timeout — provider müşteriyi tahsil etti, ama response’unuz hiç client’a ulaşmadı, şimdi tekrar deniyor. Bu, sizin API sınırınızda idempotency ile çözülür — tekrar denenen istek ikinci bir charge değil, orijinal sonucu döndürür.
Client retry — tasarım gereği güvenli olmalıdır: aynı idempotency key → aynı önbelleklenmiş response, tam durak.
Transaction ortasında database failure — DB transaction ya atomik olarak commit edilir ya da rollback edilir; commit’ten önce başarısız olursa, hiçbir şey olmamıştır ve yeniden denemek güvenlidir.
DB commit’ten sonra Kafka failure — Transactional Outbox ile çözülür (§12): event’i, payment yazımıyla aynı DB transaction’ında bir outbox tablosuna yazın ve outbox’tan at-least-once delivery + downstream’de idempotent consumer’larla publish yapan ayrı bir relay process’i olsun.
Partial failure (ledger yazımı başarılı, notification başarısız) — asimetrik kritiklik: ledger yazımı transactional ve doğru olmalıdır; notification gibi downstream etkiler, payment’ın kritik yolunu bloke etmeden, kendi retry/DLQ handling’i ile event bus üzerinden eventually consistent olmalıdır.
Akış ortasında service crash — bu tam olarak her adımın ayrı ayrı idempotent olması gerektiği ve payment state machine’inin (§5, State pattern) devam ettirilebilir olması gerektiği içindir: yeniden başlatmada, bir reconciliation/sweep job’u terminal olmayan bir durumda kalan her şeyi alır.
Duplicate webhook — PSP’ler açıkça “webhook handler’ınız idempotent olmalı” diye uyarır. İşlenen webhook event ID’lerini (provider genellikle birini sağlar) unique constraint’li bir dedup tablosunda saklayın; çakışmada, no-op.
Out-of-order event — ör. network yeniden sıralaması nedeniyle bir payment.completed webhook’u bir payment.processing webhook’undan önce gelir. State geçişlerini idempotent ve sıra-toleranslı yaparak çözülür: bir geçişi uygulamadan önce mevcut kalıcı durumu kontrol edin ve durumu körlemesine üzerine yazmak yerine geçersiz geçişleri reddeden bir state machine kullanın (§5). İsteğe bağlı olarak provider’dan monoton bir sequence/versiyon numarası ekleyin ve daha eski versiyonları görmezden gelin.
Payment PROCESSING’de takılı kaldı — zamanlanmış bir sweep job (her N dakikada bir), bir eşikten daha eski PROCESSING durumundaki payment’ları bulur, provider’dan aktif olarak durumu sorgular ve tamamlar, başarısız sayar veya manuel incelemeye eskale eder. Bu job, gerçek bir payment sisteminde pazarlık konusu değildir.
9. Idempotency
POST /payments
Idempotency-Key: abc-123
Content-Type: application/json
{"amount": 10000, "currency": "USD", "account_id": "acc_1"}
Bu tam istek 5 kez gönderilirse:
- 1. istek: mevcut key bulunmaz →
status=IN_PROGRESSve request body’nin bir hash’i ile bir satır eklenir → payment işlenir → satırstatus=COMPLETEDve response body ile güncellenir. - 2.-5. istekler (1. hâlâ işlenirken): insert denemesi unique constraint’e çarpar (
keyüzerinde) → çakışma → handler mevcut satırı okur; eğerstatus=IN_PROGRESSise, latency bütçenize bağlı olarak kısaca bloklayabilir/poll edebilir veya409 Processingdöndürebilir. - Tamamlandıktan sonraki istekler: handler
status=COMPLETEDbulur, request hash’inin eşleştiğini doğrular (key’in farklı bir payload ile yeniden kullanılmasına karşı korur — bir client bug’ı veya daha kötüsü, bir replay attack) ve yeniden işlemeden saklanan response’u aynen döndürür.
Schema
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
request_hash TEXT NOT NULL,
status TEXT NOT NULL, -- IN_PROGRESS | COMPLETED | FAILED
response_body JSONB,
response_status INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL
);
Postgres ile race’i önlemek
func (r *PaymentRepo) BeginIdempotent(ctx context.Context, key, reqHash string) (existing *IdemRecord, err error) {
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return nil, err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, `
INSERT INTO idempotency_keys (key, request_hash, status, expires_at)
VALUES ($1, $2, 'IN_PROGRESS', now() + interval '24 hours')
`, key, reqHash)
if isUniqueViolation(err) {
// Birisi bizden önce davrandı — bunun yerine mevcut satırı okuyalım.
row := tx.QueryRowContext(ctx, `SELECT status, request_hash, response_status, response_body FROM idempotency_keys WHERE key=$1`, key)
rec := &IdemRecord{}
if scanErr := row.Scan(&rec.Status, &rec.RequestHash, &rec.ResponseStatus, &rec.ResponseBody); scanErr != nil {
return nil, scanErr
}
if rec.RequestHash != reqHash {
return nil, ErrIdempotencyKeyReuse // aynı key, farklı payload — reddet
}
return rec, tx.Commit()
}
if err != nil {
return nil, err
}
return nil, tx.Commit() // race'i biz kazandık; caller payment'ı işlemeye devam eder
}
INSERT ... unique constraint mekanizmanın tamamıdır — Postgres, eşzamanlı yük altında insert’i sadece bir transaction’ın kazanacağını garanti eder; birden fazla servis instance’ı arasında hiçbir application-level lock (sync.Mutex) bunun yerine geçemez, çünkü race dağıtıktır, sadece process-içi değildir.
10. Ledger ve Double-Entry Accounting
Double-entry açıklaması
Alice, Merchant'a $100 ödüyor
Debit: Alice'in hesabı $100 (para Alice'ten çıkıyor)
Credit: Merchant'ın hesabı $100 (para Merchant'a geliyor)
Her transaction'ın entry'leri, tüm hesaplar arasında sıfıra toplanır.
Bu, kendi başına bir muhasebe geleneği değil — yerleşik bir consistency kontrolüdür: debit’ler credit’lere eşit olmazsa, bir bug’ınız var demektir ve database bu invariant’ı doğrudan zorlayabilir.
Domain modeli
type Account struct {
ID string
Currency string
}
type LedgerEntry struct {
ID string
TransactionID string // birlikte dengelenmesi gereken entry'leri gruplar
AccountID string
Direction string // "DEBIT" veya "CREDIT"
AmountMinor int64 // her zaman integer minor unit — asla float
Currency string
CreatedAt time.Time
}
type Transaction struct {
ID string
Entries []LedgerEntry
CreatedAt time.Time
}
// Balance TÜRETİLMİŞTİR, mutable bir field olarak saklanmaz.
type Balance struct {
AccountID string
AvailableMinor int64 // yerleşmiş fonlar, şimdi kullanılabilir
PendingMinor int64 // uçuştaki fonlar (hold'lar, settle edilmemiş)
}
Schema
CREATE TABLE ledger_transactions (
id UUID PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE ledger_entries (
id UUID PRIMARY KEY,
transaction_id UUID NOT NULL REFERENCES ledger_transactions(id),
account_id UUID NOT NULL REFERENCES accounts(id),
direction TEXT NOT NULL CHECK (direction IN ('DEBIT','CREDIT')),
amount_minor BIGINT NOT NULL CHECK (amount_minor > 0),
currency CHAR(3) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_ledger_entries_account ON ledger_entries(account_id, created_at);
Not: entry’ler append-only‘dir — bir ledger entry’sinde asla UPDATE olmaz. Düzeltmeler yeni, dengeleyici entry’lerdir, asla mutasyon değildir (bu size ayrıca bedavaya mükemmel bir audit trail verir).
Dengeli bir transaction’ı post etmek
func (l *LedgerService) Post(ctx context.Context, entries []LedgerEntry) error {
var sum int64
byCurrency := map[string]int64{}
for _, e := range entries {
delta := e.AmountMinor
if e.Direction == "DEBIT" {
delta = -delta
}
byCurrency[e.Currency] += delta
}
for cur, total := range byCurrency {
if total != 0 {
return fmt.Errorf("unbalanced transaction in %s: sum=%d", cur, total)
}
}
tx, err := l.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return err
}
defer tx.Rollback()
txnID := uuid.New()
if _, err := tx.ExecContext(ctx, `INSERT INTO ledger_transactions (id) VALUES ($1)`, txnID); err != nil {
return err
}
for _, e := range entries {
if _, err := tx.ExecContext(ctx, `
INSERT INTO ledger_entries (id, transaction_id, account_id, direction, amount_minor, currency)
VALUES ($1,$2,$3,$4,$5,$6)`,
uuid.New(), txnID, e.AccountID, e.Direction, e.AmountMinor, e.Currency); err != nil {
return fmt.Errorf("inserting ledger entry: %w", err)
}
}
return tx.Commit()
}
Balance’ı UPDATE accounts SET balance = balance - X yerine neden ledger’dan türetmeli?
- Auditability: mutable bir balance kolonunun geçmişi yoktur — ayrı bir transaction log’u olmadan “$47.32’ye nasıl ulaştık?” sorusunu cevaplayamazsınız, öyleyse log’u zaten doğruluk kaynağı yapabilirsiniz.
- Concurrency altında doğruluk: eşzamanlı transaction’lar altında
UPDATE ... SET balance = balance - X, kayıp güncellemeleri önlemek için hâlâ row locking gerektirir (bkz. §11) — mutable bir field’a sahip olarak concurrency problemini önlemezsiniz, sadece gizlersiniz. - Reconciliation: “ledger entry’lerinin toplamı"nı external bir provider’ın ekstresiyle karşılaştırmak doğal, mekanik bir kontroldür; tek bir mutable sayıyı karşılaştırmak size bir discrepancy’nin nerede ortaya çıktığını bulmanın hiçbir yolunu vermez.
- Recoverability: mutable bir balance gerçeklikten sapmışsa (bir bug, kötü bir migration), ledger’ı replay ederek her zaman yeniden hesaplayabilirsiniz; sadece-mutable bir balance’ın kurtarma yolu yoktur.
Pratikte, yüksek-trafikli sistemler hızlı okumalar için hâlâ materialize edilmiş bir balance (bir balances tablosu) tutar, ama bu, ledger’dan türetilen ve periyodik olarak onunla reconcile edilen bir cache’tir — asla tek doğruluk kaynağı değildir — ve ona yapılan güncellemeler ledger yazımıyla aynı transaction içinde gerçekleşir.
11. Database Tasarımı
Temel kavramlar, kısaca, hepsi fintech-ilgili
- ACID: Atomicity, Consistency, Isolation, Durability — para hareketi için pazarlık konusu değil.
- MVCC: Postgres, her transaction’a okuyucuları bloklamadan tutarlı bir snapshot verir — balance-check okumalarının yazmaları bloklamaması için kritik.
- Isolation seviyeleri:
READ COMMITTED(Postgres varsayılanı), eşzamanlı yazmalar altında “balance negatife düşemez” gibi finansal invariant’lar için genellikle yeterli değildir —SELECT ... FOR UPDATE(pessimistic) veyaSERIALIZABLE(optimistic, serialization failure’da retry ile) gerekir. - Row lock’lar /
SELECT FOR UPDATE: transaction bitene kadar seçilen satırları açıkça kilitler — aynı account’a karşı eşzamanlı debit’leri serileştirmenin standart yolu. - Optimistic locking: version kolonu +
UPDATE ... WHERE version = $1, 0-satır-etkilendi durumunda retry. Düşük-contention kaynaklar için iyi. - Pessimistic locking: önceden
SELECT FOR UPDATE. Popüler bir merchant’ın settlement account’u gibi bilinen-sıcak kaynaklar için iyi. - Partitioning: ledger_entries tabloları, yüzlerce milyon satıra ulaştığında genellikle ay veya account-ID aralığına göre partition edilir.
- Read replica’lar: hafif eskiliği tolere edebilen balance-check okumalarını devreder; gerçek debit transaction’ını asla bir replica’ya yönlendirmeyin.
Klasik concurrency bug’ı
Account balance = $100
Transaction A -> $80 çek
Transaction B -> $80 çek (neredeyse eşzamanlı gelir)
Yanlış (race condition):
// KÖTÜ: locking olmadan oku-sonra-yaz
var balance int64
db.QueryRowContext(ctx, `SELECT balance FROM accounts WHERE id=$1`, accID).Scan(&balance)
if balance < amount {
return ErrInsufficientFunds
}
db.ExecContext(ctx, `UPDATE accounts SET balance = $1 WHERE id=$2`, balance-amount, accID)
Her iki transaction da, ikisi de yazmadan önce balance=100 okuyabilir. İkisi de 100 >= 80 görür, ikisi de devam eder, iki çekimden sonra final balance -60 olur, ikincisi reddedilmeliydi. Bu ders kitabı bir kayıp güncelleme / TOCTOU bug’ıdır — ve naif bir fintech implementasyonunun bir account’un negatife düşmesine izin vermesinin tam olarak nedeni budur.
Doğru (row lock):
tx, _ := db.BeginTx(ctx, nil)
defer tx.Rollback()
var balance int64
err := tx.QueryRowContext(ctx, `SELECT balance FROM accounts WHERE id=$1 FOR UPDATE`, accID).Scan(&balance)
if err != nil { return err }
if balance < amount {
return ErrInsufficientFunds // güvenli: lock'ı biz tutuyoruz, başka hiçbir tx altımızda balance'ı değiştiremez
}
if _, err := tx.ExecContext(ctx, `UPDATE accounts SET balance = balance - $1 WHERE id=$2`, amount, accID); err != nil {
return err
}
return tx.Commit()
FOR UPDATE, transaction B’nin A commit veya rollback edene kadar bloklanmasını zorlar, böylece B’nin SELECT‘i A-sonrası balance’ı görür — ikinci çekim doğru şekilde kalan $20‘yi görür ve reddedilir.
Çoklu-account lock sıralaması (transfer’de deadlock’tan kaçınmak için): hangisi “kaynak” veya “hedef” olursa olsun, account’ları her zaman deterministik bir sırada kilitleyin (ör. account ID’ye göre sıralayın):
ids := []string{fromID, toID}
sort.Strings(ids) // tüm transaction'larda tutarlı lock sırası
for _, id := range ids {
tx.QueryRowContext(ctx, `SELECT balance FROM accounts WHERE id=$1 FOR UPDATE`, id)
}
12. Transaction Pattern’leri (Outbox, Saga)
Temel problem
DB commit başarılı
Kafka publish başarısız
Event’i, DB transaction’ını commit ettikten sonra iki ayrı işlem olarak publish ederseniz, DB’nin “payment tamamlandı” dediği ama downstream’de hiç kimsenin bunu öğrenmediği kaçınılmaz bir pencere vardır — veya tersi, önce publish sonra commit yapıyorsanız ve commit başarısız olursa, dünyaya var olmayan bir payment hakkında bilgi vermiş olursunuz.
Transactional Outbox (Go + Postgres + Kafka)
Domain satırını ve event’i tek bir DB transaction’ı içinde aynı tablo setine yazın, sonra outbox’tan asenkron olarak relay edin.
CREATE TABLE outbox_events (
id UUID PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
func (s *PaymentService) CompletePayment(ctx context.Context, p Payment) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, `UPDATE payments SET status='COMPLETED' WHERE id=$1`, p.ID); err != nil {
return fmt.Errorf("updating payment: %w", err)
}
event := PaymentCompletedEvent{PaymentID: p.ID, Amount: p.Amount, Currency: p.Currency}
payload, _ := json.Marshal(event)
if _, err := tx.ExecContext(ctx, `
INSERT INTO outbox_events (id, aggregate_id, event_type, payload)
VALUES ($1,$2,$3,$4)`,
uuid.New(), p.ID, "payment.completed", payload); err != nil {
return fmt.Errorf("writing outbox event: %w", err)
}
return tx.Commit() // payment status VE event artık atomik olarak tutarlı
}
Relay process’i (ayrı goroutine/servis, polling veya logical replication / Debezium-tarzı CDC kullanarak):
func (r *OutboxRelay) Run(ctx context.Context) {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.relayBatch(ctx)
}
}
}
func (r *OutboxRelay) relayBatch(ctx context.Context) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, event_type, payload FROM outbox_events
WHERE published_at IS NULL ORDER BY created_at LIMIT 100`)
if err != nil {
r.logger.Error("query outbox", "err", err)
return
}
defer rows.Close()
for rows.Next() {
var id uuid.UUID
var eventType string
var payload []byte
if err := rows.Scan(&id, &eventType, &payload); err != nil {
continue
}
if err := r.producer.Publish(ctx, eventType, payload); err != nil {
r.logger.Error("publish failed, will retry next tick", "id", id, "err", err)
continue // published_at NULL bırak — sonraki tick'te tekrar denenir; consumer'lar idempotent olmalı
}
r.db.ExecContext(ctx, `UPDATE outbox_events SET published_at=now() WHERE id=$1`, id)
}
}
Bu, at-least-once delivery verir — relay, publish ile published_at‘i işaretleme arasında çökerse aynı event’i iki kez publish edebilir — bu yüzden her consumer bir idempotent consumer olmalıdır (event ID’ye göre dedup, §13).
Saga pattern
Paylaşılan database’i olmayan birden fazla servise yayılan bir workflow için (ör. “Ledger’da fon rezerve et, sonra Provider’ı çağır, sonra Ledger’da onayla, hata durumunda dengeleyici aksiyonlarla”), bir Saga, her biri tanımlı bir dengeleyici aksiyona sahip bir dizi local transaction’ı koordine eder:
Adım 1: Fon rezerve et (Ledger) Dengeleme: Rezervasyonu serbest bırak
Adım 2: Provider'ı tahsil et Dengeleme: İade et
Adım 3: Ledger entry'sini onayla Dengeleme: Entry'yi ters çevir
Choreography (servisler Kafka üzerinden birbirinin event’lerine tepki verir) vs. orchestration (merkezi bir Saga coordinator servisi her adımı ve dengeleme’sini açıkça çağırır) — fintech sistemleri, özellikle payment saga’ları için genellikle orchestration‘ı tercih eder, çünkü auditability ve dengeleyici aksiyonlar üzerinde açık kontrol, choreography’nin sunduğu daha gevşek coupling’den daha önemlidir.
13. Kafka ve Event-Driven Architecture
Temel kavramlar
- Producer/Consumer/Consumer group: aynı gruptaki consumer’lar paralellik için partition’ları aralarında böler; her partition, herhangi bir zamanda grup içindeki tam olarak bir consumer tarafından okunur.
- Partition ve sıralama: Kafka sadece bir partition içinde sırayı garanti eder. Fintech etkisi: sıralama garantisine ihtiyaç duyan bir key’e göre her zaman partition edin — ör. bir account için tüm event’lerin sırayla işlenmesi için
payment.*event’leriniaccount_id‘ye göre partition edin. - Offset: consumer’ın bir partition’daki pozisyonu; offset’leri çok erken commit etmek (işlemeden önce) çökmede mesaj kaybı riski taşır; çok geç commit etmek (işleme bitmeden önce auto-commit) çökmede duplicate işleme riski taşır — bu, consumer’lar idempotent ise sorun değildir.
- At-least-once vs. at-most-once vs. “exactly-once”: Kafka’nın “exactly-once semantics"i (EOS), producer→topic→consumer pipeline’ını Kafka’nın kendi transactional API’si içinde kapsar, ama consumer’ınız yan etki olarak external bir şey yaptığı anda (bir DB yazımı, bir HTTP çağrısı), o sınır boyunca gerçek exactly-once diye bir şey yoktur — at-least-once delivery + idempotent işleme (dedup tablosu) yoluyla effectively-once elde edersiniz, ki fintech sistemlerinin gerçekte dayandığı şey budur.
- Retry / DLQ / poison message: tekrar tekrar işlenemeyen bir mesaj (kötü schema, kalıcı downstream hatası) partition’ı sonsuza kadar bloklamamalı — geri kalanının akmaya devam etmesi için N retry’dan sonra bir dead-letter topic’e yönlendirin.
- Schema evolution: producer ve consumer’lar bağımsız deploy edildiği için geriye-uyumlu evolution kurallarına (sadece opsiyonel field ekleyin, asla bir field’ı kaldırmayın veya amacını değiştirmeyin) sahip bir schema registry (Avro/Protobuf) kullanın.
Event örneği
{
"event_type": "payment.completed",
"event_id": "evt_9f8a...",
"payment_id": "pay_123",
"amount": 10000,
"currency": "USD",
"occurred_at": "2026-08-16T10:00:00Z"
}
Go’da idempotent consumer
func (c *PaymentEventConsumer) HandleMessage(ctx context.Context, msg *kafka.Message) error {
var event PaymentCompletedEvent
if err := json.Unmarshal(msg.Value, &event); err != nil {
return c.sendToDLQ(ctx, msg, fmt.Errorf("unmarshal: %w", err)) // poison message, sonsuza kadar retry etme
}
tx, err := c.db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
_, err = tx.ExecContext(ctx, `INSERT INTO processed_events (event_id) VALUES ($1)`, event.EventID)
if isUniqueViolation(err) {
return nil // zaten işlendi — offset'i commit et, yeniden işleme
}
if err != nil {
return err
}
if err := c.notifier.Send(ctx, event.PaymentID); err != nil {
return fmt.Errorf("sending notification: %w", err) // dedup insert'i de rollback et, tekrar denenecek
}
return tx.Commit()
}
Producer
func (p *KafkaProducer) Publish(ctx context.Context, eventType string, payload []byte) error {
msg := kafka.Message{
Topic: "payments.events",
Key: []byte(eventType), // veya sıralama garantileri için account_id
Value: payload,
Headers: []kafka.Header{{Key: "event_type", Value: []byte(eventType)}},
}
return p.writer.WriteMessages(ctx, msg)
}
14. Distributed Systems Temelleri
İçselleştirilecek mental model: network failure, tasarım yaptığınız edge case değil, varsayılan durumdur. Her remote çağrı timeout olabilir, geç dönebilir, bir duplicate döndürebilir veya hiçbir şey döndürmeyebilir — ve sisteminiz dört durumda da doğru olmalıdır.
- Timeout’lar: her zaman bir tane ayarlayın; payment yolunda sınırsız bir çağrı, gerçekleşmeyi bekleyen gizli bir kesintidir.
- Exponential backoff + jitter ile retry:
func retryWithBackoff(ctx context.Context, attempts int, fn func() error) error {
var err error
for i := 0; i < attempts; i++ {
if err = fn(); err == nil {
return nil
}
backoff := time.Duration(math.Pow(2, float64(i))) * 100 * time.Millisecond
jitter := time.Duration(rand.Int63n(int64(backoff / 2)))
select {
case <-time.After(backoff + jitter):
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("after %d attempts: %w", attempts, err)
}
- Circuit breaker: N ardışık hatadan sonra başarısız bir downstream’i çağırmayı durdurun, bir soğuma penceresi boyunca hızlıca başarısız olun, sonra half-open bir state ile prob edin — bir yavaş provider’ın servisinizin goroutine’lerini/bağlantılarını tüketmesini önler.
- Bulkhead: downstream başına kaynak havuzlarını izole edin (Provider A vs Provider B için ayrı connection pool’lar/semaphore’lar) böylece bir provider’ın kesintisi sağlıklı bir tanesine çağrıları aç bırakmaz.
- Rate limiting: hem kendi servisinizi hem de downstream provider’ları koruyun (birçok PSP kendi rate limit’lerini zorlar ve sizi throttle eder).
- Load balancing / service discovery / health check’ler: Kubernetes-tabanlı bir deployment’ta standart; Go servisleri
/healthz(liveness) ve/readyz(readiness — DB/Kafka bağlantıları sağlıksızsa başarısız olmalı) sunar. - Graceful shutdown: payment servislerinde kritik — bir transaction ortasında asla
SIGKILLgöndermeyin;SIGTERM‘i yakalayın, yeni istekleri kabul etmeyi durdurun, uçuştaki isteklerin bir deadline içinde bitmesine izin verin, sonra çıkın.
srv := &http.Server{Addr: ":8080", Handler: router}
go srv.ListenAndServe()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx) // yeni bağlantı almayı durdurur, uçuştakilerin bitmesini bekler
- Distributed lock’lar: cross-instance koordinasyon için Redis-tabanlı (Redlock) veya Postgres advisory lock’lar (ör. yalnızca bir instance’ın belirli bir zamanda zamanlanmış bir reconciliation job’unu çalıştırmasını sağlamak).
- Leader election: sadece-bir-aktif-instance job’lar için, genellikle Kubernetes lease object’leri veya k8s-olmayan ortamlarda
etcd/Consul üzerinden. - Pratikte CAP teoremi: ledger yazmaları, availability yerine consistency‘yi tercih eder (yanlış bir balance riski almaktansa bir transfer’i reddetmek daha iyidir); sadece-okunur balance görüntüleme, gerekirse açıkça etiketlenerek hafif eski verilerle availability‘yi tercih edebilir.
15. Go Concurrency Primitive’leri Özeti
| Primitive | Amaç | Artı | Eksi | Fintech use case |
|---|---|---|---|---|
goroutine | hafif eşzamanlı fonksiyon | ucuz, basit | sınırlandırılmaz/iptal edilmezse sızar | gelen istek başına bir tane |
channel | goroutine’ler arası iletişim | güvenli devir, doğal pipeline’lar | yanlış kullanılırsa deadlock olabilir | worker pool task dağıtımı |
sync.Mutex | karşılıklı dışlama | basit, hızlı | yüksek yükte contention | merchant config’lerinin memory-içi cache’ini korumak |
sync.RWMutex | çoklu okuyucu / tek yazar | okuma-ağırlıklı state için iyi | writer starvation mümkün | hot-reload edilen config/feature flag’ler |
sync/atomic | lock-free sayaçlar | çok hızlı | karmaşık state için yanlış kullanılması kolay | istek sayaçları, circuit-breaker hata sayıları |
sync.Once | tam olarak bir kez çalıştır | basit init pattern’i | tekrarlanan reset’ler için değil | tek seferlik client/config initialization |
sync.WaitGroup | N goroutine’i bekle | basit fan-out/join | hata propagasyonu yok | N notification göndermek, bireysel hatalar önemli değil |
errgroup.Group | hata + cancellation ile fan-out | ilk-hata propagasyonu, ctx cancel | external dependency (golang.org/x/sync) | eşzamanlı fraud+risk çağrıları (§3) |
context.Context | cancellation/deadline/değerler | standart, compose edilebilir | her yere thread edilmeli | request yolundaki her I/O çağrısı |
| worker pool | sınırlı eşzamanlı işleme | kaynak kullanımını kontrol eder | naif go fn()‘den daha fazla kod | reconciliation batch işleme |
semaphore (chan struct{}) | concurrency’yi sınırla | implement etmesi basit | manuel bookkeeping | eşzamanlı provider çağrılarını sınırlama |
Ne zaman channel vs. mutex? Paylaşılan state‘i (bir map, bir sayaç, bir struct field) birden fazla goroutine’den erişilirken korurken mutex kullanın — bu, state erişimini bir channel üzerinden yönlendirmekten daha basit ve genellikle daha hızlıdır. Goroutine’ler arasında iş veya event’leri iletirken channel kullanın — bir task’ı devretmek, tamamlanmayı sinyallemek, bir pipeline kurmak. Rob Pike’ın “share memory by communicating"i bir kılavuzdur, kanun değil — basit bir sayaç etrafında bir sync.Mutex, idiomatic Go’dur, bir anti-pattern değil.
16. API Tasarımı
POST /payments
GET /payments/{id}
POST /payments/{id}/refund
GET /payments?account_id=...&status=...&cursor=...
- Versioning: external partner-facing API’ler için en yaygın ve basit olan URL-tabanlı (
/v1/payments); header-tabanlı versioning bazen internal olarak kullanılır ama external entegratörler için sürtünme ekler. - Error format: tutarlı, yapılandırılmış hatalar, ideal olarak RFC 7807’ye (Problem Details) yakın:
{
"type": "insufficient_funds",
"title": "Insufficient funds",
"status": 422,
"detail": "Account acc_1 has insufficient available balance.",
"payment_id": "pay_123"
}
- Validation: herhangi bir business mantığına veya external çağrılara dokunmadan önce edge’de hızlıca başarısız olun (schema/tip validasyonu).
- Pagination: sınırsız büyüyen herhangi bir tablo için (payment’lar, ledger entry’leri) cursor-tabanlı (offset-tabanlı değil) — offset pagination kötü şekilde bozulur ve eşzamanlı yazmalar altında satırları atlayabilir/tekrarlayabilir.
- Idempotency: tüm mutasyon yapan finansal endpoint’lerde gerekli
Idempotency-Keyheader’ı (§9). - Rate limiting: API key/client başına,
Retry-Afterile birlikte429üzerinden döndürülür. - AuthN/AuthZ: authentication için OAuth2/OIDC bearer token’lar; ince-taneli authorization (bu API key bu merchant’ın payment’larını iade edebilir mi?) sadece gateway’de değil, istek başına zorunlu kılınır.
- Webhook’lar: her webhook payload’ını imzalayın (HMAC), alıcının idempotent işlemesi için benzersiz bir event ID ekleyin ve 2xx-olmayan durumlarda backoff ile tekrar deneyin.
17. gRPC
Payment Service ──gRPC──► Ledger Service
syntax = "proto3";
package ledger.v1;
service LedgerService {
rpc PostTransaction(PostTransactionRequest) returns (PostTransactionResponse);
rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse);
}
message LedgerEntry {
string account_id = 1;
string direction = 2; // DEBIT | CREDIT
int64 amount_minor = 3;
string currency = 4;
}
message PostTransactionRequest {
string idempotency_key = 1;
repeated LedgerEntry entries = 2;
}
message PostTransactionResponse {
string transaction_id = 1;
string status = 2;
}
Fintech’te REST vs. gRPC:
| REST/JSON | gRPC | |
|---|---|---|
| External/partner-facing API’ler | ✅ standart, evrensel tooling | nadiren, partner talep etmediği sürece |
| Internal servis-servis | kullanılabilir, daha fazla overhead | ✅ tercih edilir: güçlü tipleme, daha küçük payload’lar, HTTP/2 multiplexing |
| Streaming (ör. canlı transaction feed) | SSE/WebSocket’lerin eklenmesi gerekir | ✅ native bidirectional streaming |
| Schema evolution disiplini | daha gevşek (JSON izin vericidir) | ✅ protobuf field numaralandırma kurallarıyla zorunlu kılınır |
| Debuggability | ✅ curl edilebilir, insan-okunabilir | grpcurl/tooling gerektirir |
Çoğu fintech şirketi: edge’de (partner/client-facing) REST/JSON, Go servisleri arasında internal olarak gRPC.
18. Security
- OAuth2 / OIDC: kullanıcı ve servis authentication’ı için standart; servis-servis için client-credentials grant, kullanıcı-facing akışlar için authorization-code + PKCE.
- JWT: kısa ömürlü access token’lar, her istekte imza + expiry doğrulanır; token payload’ının kendisinde asla hassas finansal veri saklamayın.
- mTLS: cluster içinde servis-servis — her iki taraf da sertifika sunar, internal network’te olsa bile herhangi bir authenticated-olmayan pod’un ledger servisini çağırmasını önler.
- API key’ler: partner/merchant entegrasyonları için, scope’lu ve döndürülebilir, asla düz metin olarak loglanmaz.
- RBAC / ABAC: kaba izinler için (admin, support, engineer) RBAC (role-tabanlı); daha ince finansal-operasyon kontrolü için (ör. “sadece atandığı bölgedeki merchant’ların payment’larını iade edebilir”) ABAC (attribute-tabanlı).
- Secrets management: Vault veya bir cloud KMS — secret’lar asla config repo’larına commit edilen env var’larda değildir; mümkün olduğunda kısa ömürlü dinamik DB credential’ları.
- HSM: kart verisi üzerinde gerçek kriptografik key işlemleri / imzalama için — private key materyali asla HSM’den çıkmaz.
- Encryption at rest / in transit: en hassas alanlar için (PAN, SSN) DB-seviyesinde encryption at rest (genellikle cloud-provider tarafından yönetilir) artı application-seviyesinde encryption/tokenization; her yerde transit’te TLS 1.2+.
- Key rotation / secret rotation: otomatik, zamanlanmış, eski key/secret’ı kullanan uçuştaki isteklerin bozulmaması için örtüşme pencereleriyle.
- Tokenization: raw kart numaralarını, ingestion’da hemen (genellikle PSP veya özel bir vault aracılığıyla) geri döndürülemez bir token ile değiştirin, böylece sisteminizin geri kalanı — çoğu Go serviniz dahil — asla raw PAN’a dokunmaz, PCI kapsamını dramatik şekilde küçültür.
- PII koruması: field-seviyesinde encryption, katı erişim loglaması, veri minimizasyonu (ihtiyacınız olmayanı saklamayın).
- Audit logging: her finansal state değişikliğinin append-only, kurcalamaya-karşı-kanıtlı (hash-chained veya write-once bir store’a yazılmış) log’ları — kim, ne, ne zaman, nereden.
Compliance’ın mimariye etkisi
- PCI DSS: network segmentasyonunu yönlendirir — kart sahibi verisine dokunan servisler izole, daha yoğun denetlenen bir “PCI zone"da yaşar; Go servislerinizin çoğu bu zone’a asla girmeyecek şekilde mimarize edilmelidir (erkenden tokenize edin).
- SOC 2: audit logging, erişim kontrolü inceleme süreçleri ve change-management disiplinini yönlendirir — kodla ilgili değil, daha çok süreçle ilgili, ama deploy pipeline’ınızın ve erişim kontrollerinizin denetlenebilir olması gerektiği anlamına gelir.
- GDPR: veri residency’yi yönlendirir (EU müşteri verisi EU bölgelerinde), silme hakkını yönlendirir (bu “ledger entry’leri immutable’dır” ile çelişir — ledger entry’lerinde doğrudan PII saklamak yerine ledger’dan referans edilen PII’yi pseudonimize ederek/tokenize ederek çözülür).
- KYC/AML: belirli transaction eşiklerinden önce zorunlu kimlik doğrulama adımlarını ve ilk-sınıf mimari bileşenler haline gelen (sonradan akla gelen değil) transaction-monitoring/raporlama gereksinimlerini (şüpheli aktivite raporları) yönlendirir (§19).
19. Fraud ve Risk
Transaction
│
├──► Rules Engine (deterministik, düşük-latency, Go)
├──► Velocity Check (Redis-tabanlı sayaçlar, Go)
├──► Device Check (fingerprint lookup, Go)
├──► ML Score (Python/servis edilen model, gRPC üzerinden çağrılır)
├──► Historical Behavior (feature store lookup)
▼
Risk Score → karar (izin ver / step-up auth / engelle / manuel inceleme)
- Go’nun uyduğu yer: orkestrasyon katmanı, rules engine (deterministik bir if/then kural seti Go’da doğal olarak ifade edilir ve hızlıdır), velocity check’ler (Redis increment-with-TTL pattern’leri), device fingerprint lookup’ları ve — kritik olarak — bunların hepsi boyunca
context.WithTimeout+errgroupile (§3) latency bütçesini zorlamak. - Python’un kazandığı yer: model eğitimi (pandas/scikit-learn/PyTorch ekosistemi), feature engineering pipeline’ları ve takım iterasyon hızı için Python ekosisteminde kalmak istiyorsa genellikle model serving — ama production serving giderek latency nedenleriyle Go’dan gRPC üzerinden çağrılan özel bir model-serving katmanı (TF Serving, Triton veya ONNX Runtime) üzerinden yapılıyor.
- Kafka/Flink/Spark’ın devreye girdiği yer: stream’ler üzerinde gerçek-zamanlı feature hesaplama (ör. streaming aggregate olarak “son 5 dakikada kart başına transaction sayısı”) doğal bir Flink job’udur; batch feature engineering ve model eğitim verisi hazırlığı Spark’tır; Go servisleri tipik olarak bu hesaplanan feature’ların, hesaplama motorunun kendisi değil, düşük-latency bir feature store (Redis/DynamoDB-tabanlı) aracılığıyla tüketicileridir.
20. Reconciliation ve Settlement
Internal Ledger
│
▼
Reconciliation Job ◄──── External Provider Ekstresi (dosya/API)
│
▼
Eşleşme / Uyuşmazlık raporu
Uyuşmazlık örneği:
Internal: $100
Provider: $90
Adımlar:
- Asla sessizce otomatik düzeltmeyin. Discrepancy’yi tam context ile (her iki tarafta da transaction ID’leriyle) loglayın.
- Kategorize edin: zamanlama farkı (provider henüz settle etmedi — beklenen, bir sonraki döngüde çözülecek) vs. gerçek discrepancy (bir fee hesaba katılmadı, başarısız bir transaction başarılı olarak kaydedildi, bir duplicate).
- Bilinen pattern’leri otomatik çözün: ör. henüz bir ledger entry olarak post edilmemiş bilinen bir provider fee’si — iyi anlaşılan, önceden onaylanmış bir pattern’le eşleşiyorsa otomatik olarak bir düzeltme kaydı post edin.
- Geri kalanını tam bir audit trail ile manuel incelemeye eskale edin.
- Düzeltme kayıtları, orijinal transaction’a referans veren yeni ledger entry’leridir (asla geçmişi düzenlemez), append-only garantisini (§10) korur.
- Batch processing: zamanlanmış job’lar (yüksek-hacimli sistemler için genellikle gece veya gün içi), bir scheduler tarafından tetiklenen Go binary’leri olarak inşa edilir (Kubernetes CronJob, Temporal, Airflow), provider ekstre dosyalarını/API’lerini okur ve ledger ile karşılaştırır.
- Settlement/Clearing: kurumlar arasında fonların gerçek hareketi, tipik olarak bir settlement ağının programına göre batch’lenir ve işlenir (ör. ACH pencereleri) — reconciliation job’unuzun zamanlaması bu external pencerelere saygı göstermelidir.
- Retry: reconciliation sırasında geçici provider-API hataları backoff ile tekrar denenir; job’un kendisi güvenle yeniden çalıştırılabilir olmalıdır (idempotent) çünkü kısmi bir hatadan sonra tekrar çalışması gerekebilir.
21. Observability
- Structured logging: tutarlı field’larla (
request_id,payment_id,account_id)log/slog(Go 1.21’den beri standard library) — production finansal kodda aslafmt.Printlndeğil.
logger.Info("payment completed",
slog.String("payment_id", p.ID),
slog.String("account_id", p.AccountID),
slog.Int64("amount_minor", p.AmountMinor),
slog.String("trace_id", traceID),
)
- Metrics: Prometheus client kütüphanesi — endpoint/provider başına latency histogram’ları, hata-oranı sayaçları, infra metrikleriyle birlikte business metrikleri (saniye başına payment, toplam hacim).
- Distributed tracing: Go için OpenTelemetry SDK’sı, Jaeger/Tempo’ya export ederek — her istek,
context.Contextve HTTP/gRPC header’ları aracılığıyla her servis atlamasında propagate edilen bir trace ID alır. - Correlation ID / Request ID: gateway’de üretilir, her downstream çağrı boyunca propagate edilir ve her log satırına dahil edilir — bir payment’ın tam yolculuğunu sonradan yeniden inşa etmenizi sağlayan şey budur.
- Audit log’lar: operasyonel log’lardan ayrı, compliance-odaklı bir stream — hangi finansal state’i kim değiştirdi, immutable, genellikle ayrı bir write-once store’a gönderilir.
Bir payment isteğini uçtan uca trace etmek
API ──span:http.request──► Payment Service ──span:evaluate_risk──► Fraud
│
├──span:call_provider──► Provider
│
└──span:post_ledger──► Ledger
Her span aynı trace ID’yi taşır; Jaeger’da, latency’nin tam olarak nerede harcandığını gösteren tek bir waterfall diyagramı görürsünüz (ör. “provider çağrısı, toplam 900ms’lik isteğin 800ms’sini aldı”) — bu, “payment’lar bazen yavaş” durumunu bir gizemden beş dakikalık bir teşhise dönüştüren şeydir.
22. Testing
- Unit test’ler: standart
testingpaketi; domain mantığını (fee hesaplama, state geçişleri) I/O’dan arındırın, böylece kolayca unit test edilebilir. - Table-driven test’ler: birçok girdi/çıktı vakasını kısaca kapsamanın idiomatic Go pattern’i:
func TestFeeCalculation(t *testing.T) {
tests := []struct {
name string
amount Money
tier MerchantTier
wantFee Money
}{
{"standard tier", NewMoney(10000, "USD"), TierStandard, NewMoney(290, "USD")},
{"premium tier", NewMoney(10000, "USD"), TierPremium, NewMoney(190, "USD")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CalculateFee(tt.amount, tt.tier)
if got != tt.wantFee {
t.Errorf("got %v, want %v", got, tt.wantFee)
}
})
}
}
- Integration test’ler:
testcontainers-goile gerçek Postgres/Kafka ayağa kaldırın — DB constraint’inin doğruluk mekanizması olduğu ledger/idempotency kodu için kritiktir, bu yüzden DB’yi mock’lamak anlamlı bir şey test etmez. - Contract test’ler: servisinizin bir provider’ın API şekliyle ilgili varsayımlarını doğrulayın (ör. Pact kullanarak) böylece bir provider’ın breaking change’i production’da değil CI’da yakalanır.
- E2E test’ler: staging ortamı üzerinden provider sandbox’larına karşı tam payment akışı.
- Property-based test’ler: ör. “üretilen herhangi bir debit/credit entry dizisi için, ledger asla dengesiz bir transaction’ın post edilmesine izin vermez” — birçok rastgele vaka üretmek için
goptergibi bir kütüphane kullanarak. - Load test’ler: hedef throughput altında p99 latency’yi doğrulamak için staging payment servisine karşı
k6veyavegeta. - Race detector: CI’da her zaman, herhangi bir eşzamanlı fintech kodu için pazarlık konusu olmayan
go test -race— bu, bir bug sınıfının tamamını (§3) production’a ulaşmadan önce yakalar. - Chaos test’ler: staging payment akışına karşı reconciliation/sweep-job güvenlik ağlarının gerçekten çalıştığını doğrulamak için transaction ortasında kasıtlı olarak pod’ları öldürün, network latency/partition’ları enjekte edin (ör. Chaos Mesh aracılığıyla).
Para transferleri için test edilmesi gereken edge case’ler
- Duplicate request (aynı idempotency key, aynı ve farklı payload)
- Balance’ı aşan eşzamanlı çekim (klasik race, §11)
- Bilinmeyen sonuçlu provider timeout’u
- Timeout sonrası client retry
- Partial failure (DB commit oluyor, event publish başarısız)
- Duplicate webhook delivery
- Out-of-order webhook/event delivery
- Transaction ortasında database çökmesi
- Outbox relay sırasında Kafka broker’ın erişilemez olması
- Provider tamamen erişilemez (circuit breaker davranışı)
23. Production Proje Yapısı
cmd/
payment-api/main.go
outbox-relay/main.go
reconciliation-job/main.go
internal/
payment/
service.go
repository.go
http_handler.go
events.go
account/
ledger/
fraud/
reconciliation/
settlement/
pkg/
money/ # paylaşılan, export edilmesi güvenli Money value type'ı
idempotency/ # yeniden kullanılabilir idempotency middleware/helper
api/
proto/ # .proto tanımları
openapi/ # OpenAPI spec'leri
migrations/
configs/
deployments/
k8s/
terraform/
Her parçanın nedeni:
cmd/— deploy edilebilir binary başına birmain.go; binary wiring’i mantıktan ayrı tutar.internal/— “bu module’ün dışında import edilemez"i zorunlu kılar, böyleceinternal/ledger‘ın tipleri, tanımlı interface’i üzerinden geçmeden yanlışlıklainternal/payment‘a sızamaz.pkg/— gerçekten yeniden kullanılabilir, paylaşılması güvenli kod (birMoneytipi, generic helper’lar) — bilinçli olarak küçük tutulur; fintech kodunun çoğupkg/‘de değil,internal/‘da yaşamalıdır, çünkü çoğu başka yerde import edilmemeli.api/— implement eden kodla birlikte versiyonlanan interface sözleşmeleri (proto, OpenAPI).
Katmana göre paketleme vs. domain’e göre paketleme
/internal/handlers, /internal/services, /internal/repositories ← katmana göre
/internal/payment, /internal/ledger, /internal/account ← domain'e göre
Domain’e göre paketleme, önemsiz büyüklüğü geçmiş neredeyse her fintech kod tabanında kazanır, çünkü:
- “Payment’ların nasıl çalıştığı"na yapılan bir değişiklik üç değil, bir dizini etkiler.
- Doğrudan DDD bounded context’lerine (§7) eşlenir ve microservice’lere bölünürseniz,
internal/paymentneredeyse doğrudan çıkarılabilir bir adaydır. ledger‘ınpayment‘ın internal’larına bağımlı olmaması gerektiği DDD kuralını doğal olarak zorunlu kılar — Go’nuninternal/visibility kuralları bunu sadece bir konvansiyon değil, compiler tarafından zorlanan bir sınır yapar.
Katmana göre paketleme ne zaman uygundur: domain sınırı overhead’inin henüz değmediği gerçekten küçük bir servis (birkaç endpoint, bir takım, kısa beklenen ömür).
24. Go Anti-Pattern’leri (Özellikle Java/C#/Node’dan Geçenler İçin)
| Anti-pattern | Kötü | İyi |
|---|---|---|
| Overengineering | Tek provider’lı bir servis için 5 abstraction katmanlı bir plugin mimarisi kurmak | Abstraction’ı ikinci provider’ı entegre ederken ekleyin, öncesinde değil |
| Devasa interface’ler | type Repository interface { /* 20 method */ } | PaymentReader, PaymentWriter‘a bölün, her biri 1-3 method’lu |
| Her yerde interface | Hiç ikinci bir implementasyonu olmayan tek bir implementasyonlu fonksiyon için type Calculator interface{ Calculate() int } | Sadece bir fonksiyon kullanın; gerçekten ikinci bir implementasyon veya test double gerektiğinde interface’i ekleyin |
| Aşırı abstraction | Bir PaymentStrategyFactoryProviderResolver | Bir switch ifadesi veya bir map — Go doğrudanlığı ödüllendirir |
| Derin paket hiyerarşisi | internal/domain/payment/entities/aggregates/payment | internal/payment |
| Global state | Her yerden erişilen var GlobalDB *sql.DB | Servis başına constructor-injected *sql.DB |
| Goroutine leak’leri | go func() { <-neverClosedChannel }() | Her zaman ctx.Done() üzerinde select yapın veya iptal edilebilir bir channel kullanın |
| Yok sayılan hatalar | Bir payment yolunda _ = ledger.Post(ctx, entry) | Her hata kontrol edilir, wrap edilir ve ya handle edilir ya da açıkça propagate edilir |
| Context yanlış kullanımı | context.Value‘da *sql.DB veya business veri saklamak | Context sadece cancellation/deadline/trace metadata’sı taşır; gerçek dependency’leri açıkça geçirin |
| Channel’ın aşırı kullanımı | Basit bir sayacı korumak için channel kullanmak | Bir sync/atomic sayaç veya sync.Mutex daha basit ve genellikle daha hızlıdır |
| Singleton kötüye kullanımı | Her servis package-seviyesinde bir singleton | Constructor’lar aracılığıyla açık construction ve dependency injection |
| Generic utility paketleri | Çöp kutusu haline gelen bir utils veya common paketi | Helper’ları sahibi olan domain paketine koyun; sadece gerçekten paylaşıldığında pkg/‘e çıkarın |
| God servisler | Payment’lar, iade’ler, disputes ve settlement’a yayılan 40 method’lu tek bir PaymentService | Gerçek alt-domain’lere göre bölün: PaymentService, RefundService, DisputeService |
| God struct’lar | Olası her payment tipini ve durumunu kapsayan 60 field’lı bir Payment struct’ı | Embedding ile compose edin veya Payment + tipe-özel extension struct’larına bölün |
25. Performance
- Allocation: hot path’lerde (payment validasyonu, ledger posting) allocation’ları minimize edin — buffer’ları yeniden kullanın, gereksiz
[]byte↔stringdönüşümlerinden kaçının, struct’ları sadece büyükse veya mutasyon gerektiğinde pointer ile geçirmeyi tercih edin (küçük struct’lar genellikle heap-allocate edip pointer-chase etmekten daha ucuza kopyalanabilir). - Garbage collector: Go’nun GC’si düşük duraklama sürelerini hedefler (1.5+ concurrent collector’dan beri tipik sub-milisaniye duraklamalar, sürümler boyunca daha da ayarlanmış);
GOGCveGOMEMLIMIT, latency-hassas payment servisleri için GC agresifliği vs. memory boşluğu dengesini ayarlamak için iki koldur. - Escape analysis: heap’e nelerin kaçtığını görmek için
go build -gcflags="-m"kullanın; gereksiz yere kaçan bir değer (ör. concrete bir tip yeterliyken interface olarak döndürülen) GC baskısı ekler. - CPU profiling /
pprof: on-demand CPU/heap/goroutine profiling için staging/prod’da (auth arkasında!) mount edilennet/http/pprof— “neden p99 latency yükseldi” olaylarını teşhis etmek için paha biçilmez. - Benchmarking:
func BenchmarkLedgerPost(b *testing.B) {
svc := setupTestLedger(b)
entries := sampleBalancedEntries()
b.ResetTimer()
for i := 0; i < b.N; i++ {
svc.Post(context.Background(), entries)
}
}
ns/op ile birlikte allocation/op’u görmek için go test -bench=. -benchmem ile çalıştırın.
sync.Pool: GC baskısını azaltmak için yüksek throughput altında kısa ömürlü nesneleri (ör. JSON encoding buffer’ları) yeniden kullanın — ama önce profile yapın; erken pooling, çoğu serviste marjinal kazançlar için karmaşıklık ekler.- Connection pooling:
db.SetMaxOpenConns/SetMaxIdleConns‘u bilinçli olarak ayarlayın — sınırsız bir pool, yük artışları altında Postgres’i bunaltabilir; çok küçük bir pool kendisi darboğaz haline gelir. - Batch processing: domain izin verdiğinde DB yazmalarını (multi-row
INSERT) ve Kafka produce’larını batch’leyin (ör. §12’deki outbox relay zaten her tick’te 100 satır batch’liyor).
26. Scalability
100 req/s → 10.000 req/s → 100.000 req/s
- 100 req/s: iyi yapılandırılmış tek bir instance, mütevazı bir Postgres instance’ı ile bunu rahatlıkla halleder. Ölçeğe değil, doğruluğa odaklanın.
- 10.000 req/s: stateless servislerin yatay ölçeklenmesi norm haline gelir (CPU/custom metric’lerde Kubernetes HPA); database gerçek kısıtlama haline gelir — balance-check okumaları için read replica’lar, connection pooling (PgBouncer) getirin ve consumer instance’ları arasında paralellik için Kafka topic’lerini partition etmeye başlayın.
- 100.000 req/s: ledger’ın yazma yolu genellikle zor darboğazdır — account’ları account ID aralığı veya hash’e göre birden fazla Postgres instance/cluster’ına sharding etmek gerekli hale gelir; asenkron işlemenin yoğun kullanımı (sadece ledger yazımı ve idempotency check senkron/bloklayıcı kalır; notification’lar, analytics, çoğu yan etki Kafka üzerinden gider); okuma-ağırlıklı, hafif-eski-tolere-edilebilir veriler (merchant config’leri, rate limit’leri) için agresif caching (Redis); paralelliği gerçekten kullanılabilir tutmak için Kafka partition sayısına karşı consumer instance sayısına dikkatli özen.
Kollar, kabaca fintech takımlarının onlara ulaşma sırasına göre:
- Orkestrasyon servislerinin stateless yatay ölçeklenmesi (ucuz, önce bunu yapın).
- Okuma-ağırlıklı, kritik-olmayan-yol verileri için caching (Redis).
- Database için read replica’lar.
- Kritik consistency yolunda olmayan her şey için Kafka üzerinden asenkron işleme.
- Connection pooling ayarı (PgBouncer, dikkatli
MaxOpenConns). - Database sharding/partitioning (pahalı, bunu en sona bırakın ve ID şemanızı bunun için erken tasarlayın, henüz shard etmeseniz bile — bir sharding key’i sonradan eklemek, gün birinden şemanıza ekmekten çok daha zahmetlidir).
27. Tam Örnek: “Payment & Ledger Platform”
┌───────────────┐
│ API Gateway │
└───────┬───────┘
┌────────────────┼────────────────┐
┌───────▼──────┐ ┌───────▼───────┐ ┌───────▼────────┐
│Payment Service│ │Account Service│ │ Webhook Service │
└───┬───┬───┬───┘ └───────┬───────┘ └────────┬────────┘
│ │ │ │ │
┌──────▼┐ ┌▼───────┐ ┌───────▼──┐ ┌───────▼───────┐
│Fraud │ │Ledger │ │ Provider │ │ Reconciliation│
│Service│ │Service │ │ Adapter │ │ Service │
└───────┘ └────┬────┘ └────┬─────┘ └───────┬───────┘
│ │ │
┌────▼───────────▼──────────────────────▼────┐
│ Kafka (event bus) │
└────┬─────────────────────────────────┬──────┘
┌─────▼─────┐ ┌──────▼───────┐
│Notification│ │ PostgreSQL / │
│ Service │ │ Redis / S3 │
└────────────┘ └────────────────┘
- Database schema:
payments,idempotency_keys,ledger_transactions,ledger_entries,outbox_events,accounts,balances,processed_events(consumer dedup),reconciliation_reports. - Domain modeli:
Payment(aggregate root) →PaymentAttemptentity’leri,Moneyvalue object’i;LedgerTransaction(aggregate root) →LedgerEntryentity’leri. - API tasarımı: edge’de REST (
/v1/payments), internal olarak gRPC (PaymentService → LedgerService,PaymentService → FraudService). - Event schema:
payment.created,payment.completed,payment.failed,ledger.posted, bir schema registry üzerinden versiyonlanmış. - Go paket yapısı: §23’teki gibi,
internal/altında domain’e göre paketleme. - Kullanılan design pattern’ler: Adapter (provider entegrasyonları), Decorator (retry/metrics sarmalama), Strategy (fee hesaplama), State (payment lifecycle’ı), Chain of Responsibility (HTTP middleware), Factory (provider seçimi).
- Hata yönetimi: sentinel + custom hatalar,
%wile wrap edilmiş, her yerdeerrors.Is/errors.Asile kontrol edilmiş. - Concurrency modeli: fraud/risk için
errgroupfan-out (§3), reconciliation ve notification fan-out için worker pool’lar, provider çağrı concurrency’si için sınırlı semaphore’lar. - Security modeli: internal olarak mTLS, edge’de OAuth2/OIDC, PCI kapsamını minimize etmek için ingestion’da tokenization, Vault-yönetilen secret’lar.
- Testing stratejisi: domain mantığı için table-driven unit test’ler, ledger/idempotency için testcontainers-tabanlı integration test’ler, CI’da
-race, büyük lansmanlardan önce load test’ler, üç ayda bir chaos test’ler. - Observability stratejisi: her atlamada OpenTelemetry tracing, Prometheus metrics, correlation ID’li structured
sloglogging, ayrı immutable audit log stream’i.
28. Production-Quality Kod Örneği: Uçtan Uca Payment Handler
// PaymentHandler HTTP'yi application service'e bağlar, tasarım gereği ince kalır.
type PaymentHandler struct {
svc *PaymentService
logger *slog.Logger
}
func (h *PaymentHandler) CreatePayment(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
traceID := trace.SpanFromContext(ctx).SpanContext().TraceID().String()
logger := h.logger.With(slog.String("trace_id", traceID))
idemKey := r.Header.Get("Idempotency-Key")
if idemKey == "" {
writeProblem(w, http.StatusBadRequest, "missing_idempotency_key", "Idempotency-Key header is required")
return
}
var req CreatePaymentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid_request", err.Error())
return
}
if err := req.Validate(); err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
return
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result, err := h.svc.ProcessPayment(ctx, idemKey, req)
switch {
case errors.Is(err, ErrIdempotencyKeyReuse):
writeProblem(w, http.StatusConflict, "idempotency_key_reuse", "key reused with a different payload")
case errors.Is(err, ErrInsufficientFunds):
writeProblem(w, http.StatusUnprocessableEntity, "insufficient_funds", "account has insufficient available balance")
case err != nil:
logger.Error("payment processing failed", slog.String("error", err.Error()))
writeProblem(w, http.StatusInternalServerError, "internal_error", "an unexpected error occurred")
default:
writeJSON(w, http.StatusCreated, result)
}
}
// PaymentService, use case'i orkestre eden application service'tir.
type PaymentService struct {
db *sql.DB
idemRepo *IdempotencyRepo
ledger LedgerClient
fraud FraudClient
risk RiskClient
provider PaymentProvider
outbox *OutboxWriter
logger *slog.Logger
}
func (s *PaymentService) ProcessPayment(ctx context.Context, idemKey string, req CreatePaymentRequest) (*PaymentResult, error) {
reqHash := hashRequest(req)
existing, err := s.idemRepo.BeginIdempotent(ctx, idemKey, reqHash)
if err != nil {
return nil, fmt.Errorf("idempotency check: %w", err)
}
if existing != nil {
return existing.ToResult(), nil // tekrarlanan response, yeniden işleme yok
}
payment := NewPayment(req.AccountID, req.AmountMinor, req.Currency)
riskCtx, cancel := context.WithTimeout(ctx, 150*time.Millisecond)
defer cancel()
risk, err := s.evaluateRisk(riskCtx, payment)
if err != nil {
s.logger.Warn("risk evaluation degraded, proceeding with conservative default", slog.String("payment_id", payment.ID))
risk = RiskResult{Score: DefaultConservativeScore, Degraded: true}
}
if risk.Score > BlockThreshold {
s.idemRepo.Complete(ctx, idemKey, http.StatusForbidden, ErrBlockedByRisk)
return nil, ErrBlockedByRisk
}
if err := payment.TransitionTo(StateProcessing); err != nil {
return nil, fmt.Errorf("invalid state transition: %w", err)
}
chargeResult, err := s.provider.Charge(ctx, ChargeRequest{
IdempotencyKey: idemKey, // provider'a da geçirin — provider'lar bunu açıkça destekler
AmountMinor: payment.AmountMinor,
Currency: payment.Currency,
})
if err != nil {
if errors.Is(err, ErrProviderTimeout) {
// Bilinmeyen sonuç — FAILED olarak işaretleME. PROCESSING'de bırak; sweep job (§8) çözer.
s.logger.Error("provider timeout, payment left in PROCESSING for sweep resolution",
slog.String("payment_id", payment.ID))
return nil, fmt.Errorf("provider timeout, payment pending resolution: %w", err)
}
payment.TransitionTo(StateFailed)
s.idemRepo.Complete(ctx, idemKey, http.StatusUnprocessableEntity, err)
return nil, fmt.Errorf("provider charge failed: %w", err)
}
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return nil, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
if err := s.postLedgerEntries(ctx, tx, payment, chargeResult); err != nil {
return nil, fmt.Errorf("posting ledger entries: %w", err)
}
payment.TransitionTo(StateCompleted)
if err := s.savePayment(ctx, tx, payment); err != nil {
return nil, fmt.Errorf("saving payment: %w", err)
}
if err := s.outbox.Write(ctx, tx, "payment.completed", PaymentCompletedEvent{PaymentID: payment.ID}); err != nil {
return nil, fmt.Errorf("writing outbox event: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("commit: %w", err)
}
result := payment.ToResult()
s.idemRepo.Complete(ctx, idemKey, http.StatusCreated, nil)
return result, nil
}
Neden bu şekilde yazıldı:
context.Context, her I/O çağrısı boyunca thread edilmiş — her çağrı iptal edilebilir ve sınırlıdır.- Her katmanda
%wile wrap edilmiş hatalar — “ne başarısız oldu ve neden"inpprof-tarzı bir yığını, tek bir hata string’inden yeniden inşa edilebilir. - Idempotency, herhangi bir yan etkiden önce kontrol edilir ve (önbelleklenmiş response ile) sonra tamamlanır — tüm handler uçtan uca tekrar denemeye güvenlidir.
- Provider-timeout dalı, açıkça bir sonucu tahmin etmez — payment’ı bilinçli olarak sweep job için ara bir durumda bırakır, “yardımcı olmak için” başarısız olarak işaretleyip daha sonra çift-tahsilat riski almak veya tamamlandı olarak işaretleyip asla alınmamış para için bir ledger’ı kredilendirmek riski almak yerine.
- Ledger yazımı + payment kaydetme + outbox yazımı tek bir DB transaction’ındadır — üçü boyunca atomicity, böylece diğerleri olmadan birinin başarılı olduğu bir pencere yoktur (§12).
- Her log çağrısında
trace_idile structured logging — bir olayı production’da izlenebilir kılan tek satır budur.
29. Trade-off Analizi
Go vs. Java — Go: daha hızlı başlangıç, daha düşük memory footprint, daha basit concurrency modeli, büyük kod tabanları için daha hızlı derleme süreleri, JVM ops overhead’i yok. Java: ağır enterprise entegrasyonu için olgun ekosistem (Spring), çok büyük takım-ölçekli refactoring için daha güçlü tooling (bazıları savunur), JIT bazı iş yükleri için ham sürekli CPU-bound throughput’ta Go’yu geçebilir. Çoğu fintech, yeni payment-yolu servisleri için Go’yu seçer ve halihazırda var olan yerlerde (core banking, büyük enterprise entegrasyonları) tamamen yeniden yazmak yerine Java’yı tutar.
Go vs. Rust — Rust: hiç GC yok, en düşük latency, en fazla memory-kısıtlı yollar için sınıfının en iyisi (bazı crypto/blockchain çekirdekleri, bazı card-auth hot path’leri). Go: işe almak ve onboard etmek dramatik şekilde daha hızlı, zaman baskısı altında doğru kod göndermek daha hızlı, GC duraklamaları sub-mikrosaniye-latency-kritik olmayan fintech servislerinin büyük çoğunluğu için önemsizdir. Dar, haklı bir hot path için Rust’ı, sistemin diğer %95’i için Go’yu seçin.
Go vs. Kotlin — Kotlin: zaten Spring altyapısına sahip bir JVM şirketiyseniz, payment servislerini Kotlin’de tutmak, ikinci bir runtime/deployment stack’i işletmekten kaçınır. Go: Kubernetes-native infra tooling’de (aynı zamanda Go) standartlaşıyorsanız ve daha küçük, daha hızlı başlayan container’lar istiyorsanız daha iyi uyum. Genellikle teknik olmaktan çok bir takım-yeteneği ve mevcut-altyapı kararı.
Go vs. Python — Python: ML/veri-bilimi-ağırlıklı fraud/risk çalışması ve hızlı prototipleme için yenilmez. Go: eşzamanlı, latency-hassas orkestrasyon ve transactional çekirdek için yenilmez. Çoğu olgun fintech, birini seçmek yerine, bilinçli olarak ikisini de çalıştırır.
PostgreSQL vs. MongoDB — Postgres: ACID transaction’lar, güçlü consistency, ledger’ların gerektirdiği row-locking/serializable pattern’leri için olgun destek. MongoDB: esnek schema, kutudan çıktığı gibi daha kolay bir yatay ölçekleme hikayesi — ama multi-document ACID transaction’lar (MongoDB 4.0’dan beri mevcut) çoğu fintech risk değerlendirmesinde ledger-seviyesi doğruluk için hâlâ daha az savaş-test edilmiştir. Özellikle ledger/para-hareketi verisi için, Postgres (veya özel bir ledger database’i) fintech’te varsayılan seçime yakındır; MongoDB, transactional-olmayan veri (log’lar, device fingerprint’leri, yapılandırılmamış metadata) için daha yaygındır.
Kafka vs. RabbitMQ — Kafka: yüksek-throughput, replay edilebilir, partition-başına-sıralı event log’ları için inşa edilmiş — domain event’leri ve audit-ilgili stream’ler için doğal uyum (geçmişi replay edebilirsiniz). RabbitMQ: daha basit operasyonel model, karmaşık routing topolojileri ve öncelik kuyrukları için güçlü destek, genellikle event-sourcing-tarzı bir log’dan çok düşük-hacimli task-queue-tarzı iş yükleri için daha iyi bir uyum. Ölçekte event-driven mimari yapan fintech’ler Kafka’ya yönelir; daha basit task dispatch bazen RabbitMQ’da veya hatta Postgres-tabanlı bir kuyrukta kalır.
REST vs. gRPC — bkz. §17.
Redis vs. PostgreSQL — Redis: sub-milisaniye okumalar, velocity sayaçları, rate limit’ler, session/cache verisi için ideal — para için bir doğruluk kaynağı değil. Postgres: durability ve transactional doğruluk gerektiren her şey için doğruluk kaynağı. Yaygın pattern, dayanıklı ledger olarak Postgres’in önünde hızlı, atılabilir bir cache/sayaç katmanı olarak Redis’tir — asla tersi değil.
Microservices vs. Modular Monolith — bkz. §6. Bölünmek için gerçek bağımsız-ölçekleme veya bağımsız-takım-sahipliği nedeniniz olana kadar modular monolith’e varsayılan olarak gidin.
CQRS vs. geleneksel CRUD — CQRS, okuma ve yazma modelleri gerçekten ayrıştığında özellikle kazandırır (ledger: katı double-entry yazmalar vs. denormalize edilmiş bir balance-summary okuması) — basit bir CRUD kaynağı için (merchant profil ayarları) getirmek genellikle gereksiz karmaşıklıktır.
Event Sourcing vs. normal database — Event Sourcing size “bedavaya” mükemmel bir audit trail ve point-in-time replay verir, ki bu ledger’ın doğası gereği zaten neden event-sourced olarak tasarlandığıdır — ama tam ES makinesini (event store, projection’lar, snapshot’lar) sistemdeki her domain’e uygulamak yaygın bir overengineering tuzağıdır; bunu, geçmişin kendisinin ürün olduğu domain’ler için ayırın (ledger), sadece mevcut duruma ihtiyacınız olan domain’ler için değil (çoğu CRUD entity’si).
30. Öğrenme Roadmap’i
Seviye 1 — Go Temelleri
- Konular: syntax, tipler, slice’lar/map’ler, struct’lar, interface’ler, temel hata yönetimi, module’ler.
- Projeler: bir CLI aracı; memory-içi storage’lı basit bir REST API.
- Kaynaklar: resmi Go Tour, “The Go Programming Language” (Donovan/Kernighan).
- Çözülmesi gereken problemler: eşzamanlı-güvenli erişime sahip basit bir memory-içi key-value store implement edin.
- Bir sonraki seviyeye geçiş kapısı: başka bir dilden mental olarak çeviri yapmadan idiomatic Go yazarken rahatsınız.
Seviye 2 — Idiomatic Go
- Konular: §4’ün tamamı — kompozisyon, küçük interface’ler, functional options, hata wrapping, context.
- Projeler: Seviye 1 API’nizi constructor injection ve küçük interface’ler kullanarak refactor edin; her yere düzgün hata wrapping ekleyin.
- Kaynaklar: “Effective Go,” Uber’in Go Style Guide’ı, Go Proverbs (Rob Pike).
- Çözülmesi gereken problemler: naif bir singleton-ağırlıklı tasarımı açık dependency injection ile değiştirin.
- Kapı: başka birinin Go kodunu inceleyebilir ve idiomatic-olmayan pattern’leri tespit edebilirsiniz.
Seviye 3 — Backend Go
- Konular:
net/http, routing, middleware (Chain of Responsibility, §5), JSON handling, structured logging, graceful shutdown. - Projeler: auth middleware, rate limiting, structured log’lar ve
SIGTERM‘i handle eden graceful shutdown içeren bir REST API. - Kaynaklar: “Let’s Go” (Alex Edwards).
- Çözülmesi gereken problemler: idempotency-key middleware’ini sıfırdan implement edin.
- Kapı: sadece bir oyuncak API değil, production-şeklinde bir Go HTTP servisi inşa edebilir ve deploy edebilirsiniz.
Seviye 4 — PostgreSQL + Transaction’lar
- Konular: §11’in tamamı — isolation seviyeleri, row locking,
SELECT FOR UPDATE, optimistic vs pessimistic locking, migration’lar. - Projeler: §11’deki double-withdrawal race condition’ını implement edin, bug’ı yeniden üretin, sonra row locking ile düzeltin; bunu otomatik eşzamanlı bir test olarak yazın.
- Kaynaklar: “Designing Data-Intensive Applications” (Kleppmann) — transaction’lar üzerine bölümler.
- Çözülmesi gereken problemler: gerçek bir load test altında yeniden üretilen ve düzeltilen klasik eşzamanlı-çekim bug’ı.
- Kapı: somut bir örnekle,
READ COMMITTED‘in finansal yazmalar için neden otomatik olarak güvenli olmadığını açıklayabilirsiniz.
Seviye 5 — Kafka + Event-Driven Sistemler
- Konular: §12-13’ün tamamı — outbox pattern, idempotent consumer’lar, partitioning, schema evolution.
- Projeler: payment→outbox→relay→Kafka→idempotent-consumer pipeline’ını uçtan uca inşa edin, batch ortasında relay’i öldüren bir chaos test dahil.
- Kaynaklar: Kafka: The Definitive Guide.
- Çözülmesi gereken problemler: bir duplicate event delivery’sini simüle edin ve consumer’ınızın bunu doğru şekilde handle ettiğini kanıtlayın.
- Kapı: bir servis sınırını geçen herhangi bir şey için “exactly-once"un neden yanıltıcı bir terim olduğunu açıklayabilirsiniz.
Seviye 6 — Distributed Systems
- Konular: §14’ün tamamı — circuit breaker’lar, retry’lar/backoff/jitter, bulkhead’ler, CAP teoremi trade-off’ları, graceful shutdown, distributed lock’lar.
- Projeler: kararsız bir downstream dependency’yi bir circuit breaker + sınırlı retry ile sarın ve zarif şekilde başarısız olduğunu load-test edin.
- Kaynaklar: “Release It!” (Michael Nygard).
- Çözülmesi gereken problemler: bir downstream servis timeout’unuzdan önce hiçbir şey döndürmediğinde doğru kalan bir sistem tasarlayın.
- Kapı: “network failure normaldir” artık sonradan akla gelen bir şey değil, varsayılan tasarım varsayımınızdır.
Seviye 7 — Fintech Domain
- Konular: §7, §9, §10, §19, §20 — fintech için DDD, idempotency, double-entry ledger’lar, fraud/risk, reconciliation.
- Projeler: dengesiz bir transaction’ın asla post edilemeyeceğini kanıtlayan property-based bir test ile §10’daki tam ledger servisini inşa edin.
- Kaynaklar: payment-endüstrisi blog’ları (Stripe engineering blog, Adyen tech blog), “Accounting for Computer Scientists” (public online yazı).
- Çözülmesi gereken problemler: kasıtlı uyuşmazlıklarla simüle edilmiş bir provider ekstresine karşı reconciliation eşleştirme mantığını implement edin.
- Kapı: balance’ların neden bir ledger’dan türetilmesi gerektiğini, somut bir hata senaryosu kullanarak teknik-olmayan bir paydaşa açıklayabilirsiniz.
Seviye 8 — Production Architecture
- Konular: §2, §6, §23 — referans mimari, Clean/Hexagonal trade-off’ları, paket yapısı.
- Projeler: gerçek Kubernetes deployment manifest’leriyle tam “Payment & Ledger Platform"u (§27) bir araya getirin.
- Kaynaklar: “Building Microservices” (Sam Newman).
- Çözülmesi gereken problemler: Clean Architecture töreninin bilinçli olarak uygulamamayı seçtiğiniz yeri ve nedenini yazılı olarak gerekçelendirin.
- Kapı: şüpheci bir staff mühendisin incelemesinde bir mimari trade-off kararını yapabilir ve savunabilirsiniz.
Seviye 9 — Security ve Compliance
- Konular: §18’in tamamı — PCI DSS scope azaltma, tokenization, KMS/HSM, audit logging, GDPR vs. immutable ledger’lar.
- Projeler: hassas bir field için field-seviyesinde tokenization implement edin ve sonuçtaki veri akışında azaltılmış PCI scope’unu gösterin.
- Kaynaklar: PCI DSS Quick Reference Guide (resmi PCI SSC dokümanı), OWASP kaynakları.
- Çözülmesi gereken problemler: GDPR’nin silme hakkını, hiçbir gereksinimi bozmadan append-only bir ledger tasarımıyla uzlaştırın.
- Kapı: bir denetçiyi sisteminizin PCI scope sınırından geçirebilir ve gerekçelendirebilirsiniz.
Seviye 10 — Staff/Principal-Seviyesi Mimari
- Konular: bir organizasyonun tüm fintech platformu boyunca çapraz-kesen trade-off yargısı (§29); servisleri ne zaman bölmeli; teknik borcu ne zaman bilinçli olarak kabul etmeli; mühendisleri §1-29 boyunca mentörlük etme.
- Projeler: gerçek bir migration için (ör. monolith → hedefli microservice çıkarımı) dokümante edilmiş trade-off’lar ve bir rollback planıyla gerçek bir architecture decision record (ADR) sürecine liderlik edin.
- Kaynaklar: internal postmortem’ler (kendiniz ve public olanlar — Stripe, Monzo ve diğerleri incident retrospektifleri yayınlar), “A Philosophy of Software Design” (Ousterhout).
- Çözülmesi gereken problemler: canlı, gelire-kritik bir payment yolu için sıfır kesinti ve her adımda güvenli bir rollback ile bir migration planı tasarlayın.
- Gerçekten Staff-seviyesi olma kapısı: artık diğer mühendislerin §1-29 trade-off sorularını getirdiği kişisiniz ve bu dokümandaki her karar için sadece neyi değil, nedenini de açıklayabiliyorsunuz.
31. Master Checklist
1. Bilmem gereken Go pattern’leri
- Functional options
- Küçük interface’ler + kompozisyon
- Constructor injection (DI framework yok)
- Adapter (provider entegrasyonları)
- Decorator (retry/metrics sarmalama)
- Fonksiyon tipi olarak Strategy
- Lifecycle entity’leri için state machine pattern’i
- HTTP middleware için Chain of Responsibility
- Pluggable implementasyonlar için Factory / registry
2. Bilmem gereken distributed-system pattern’leri
- Idempotency key’ler (client-üretilmiş, DB-zorunlu)
- Transactional Outbox
- Saga (orchestration vs. choreography)
- Circuit breaker
- Bulkhead izolasyonu
- Exponential backoff + jitter ile retry
- Distributed lock’lar (Redis/Postgres advisory lock’lar)
- Singleton job’lar için leader election
- CQRS
3. Bilmem gereken fintech pattern’leri
- Double-entry ledger (append-only, dengeli transaction’lar)
- Mutable state değil, bir projection olarak balance
- Idempotent webhook handling
- Takılı durumlar için sweep job’lu payment state machine’i
- Kategorize edilmiş uyuşmazlık handling’i ile reconciliation
- Düzeltme kayıtları (asla geçmişi düzenlemeyin)
- Güvenli fallback ile sınırlı-latency fraud/risk check’leri
4. Bilmem gereken teknolojiler
- PostgreSQL (isolation seviyeleri, locking, partitioning)
- Kafka (veya eşdeğer event log)
- Redis (caching, velocity sayaçları, rate limiting)
- gRPC + protobuf
- OpenTelemetry, Prometheus, Grafana, Jaeger
- Kubernetes temelleri (deployment’lar, HPA, CronJob’lar, health check’ler)
- Vault veya eşdeğer secrets manager
5. Bilmem gereken Go idiom’ları
- Açık hata yönetimi, wrapping,
errors.Is/errors.As - Her I/O çağrısı boyunca
context.Contextthread etme - Cleanup/rollback güvenlik ağları için
defer - Domain’e göre paketleme,
internal/visibility disiplini - Table-driven test’ler
- Zero-value farkındalığı (ve güvenlik için bilinçli olarak ne zaman bozulmalı, ör.
Money)
6. Bilmem gereken database konuları
- ACID, MVCC, isolation seviyeleri
-
SELECT FOR UPDATEvs. optimistic locking - Deterministik multi-row lock sıralaması
- Yüksek-hacimli tablolar için partitioning stratejisi
- Read replica’lar ve eskilik trade-off’ları
7. Bilmem gereken Kafka/event-driven konuları
- At-least-once delivery + idempotent consumer’lar = effectively-once
- Sıralama garantileri için partition key seçimi
- DLQ / poison message handling
- Schema evolution disiplini (geriye-uyumluluk)
8. Bilmem gereken security/compliance konuları
- PCI scope’unu minimize etmek için tokenization
- Internal servisler arasında mTLS
- Finansal operasyonlar için RBAC vs. ABAC
- Audit logging (append-only, kurcalamaya-karşı-kanıtlı)
- GDPR silme vs. immutable ledger reconciliation stratejisi
9. Portfolio için yapmam gereken projeler
- Property-based balance test’leriyle tam bir double-entry ledger servisi
- Gerçek bir duplicate-request test suite’ine sahip idempotent bir payment API’si
- Hiçbir event’in kaybolmadığını kanıtlayan bir chaos test’e sahip bir transactional-outbox + Kafka relay
- Kasıtlı uyuşmazlıklarla simüle edilmiş bir provider ekstresine karşı bir reconciliation job’u
- Hedef throughput altında p99 latency’yi gösteren, başarısız bir dependency’den kanıtlanabilir şekilde koruyan bir circuit breaker’a sahip, load-test edilmiş bir payment orchestration servisi
10. Senior → Staff’a geçmek için öğrenmem gereken konular
- Architecture Decision Record’ları (ADR’ler) yazmak ve savunmak
- Clean/Hexagonal töreninin ne zaman kendi maliyetine değdiğini yargılamak (§6) — ve “hayır"ı geçerli bir mimari kararı olarak savunmak
- Gelire-kritik yollarda sıfır-kesinti migration’lara liderlik etmek
- Sadece “nasıl"ı değil, diğer mühendisleri §29’daki trade-off akıl yürütmesi boyunca mentörlük etmek
- Kendi kesintilerinizin ötesinde failure-mode sezgisi inşa etmek için payment şirketlerinin public incident postmortem’lerini okumak ve içselleştirmek
Bu dokümanın amacı size Go öğretmek değil. Amacı, bir Go mühendisinin bir fintech production sisteminde nasıl düşündüğünü öğretmek: network çağrılarından şüpheci, her hataya karşı açık, gizli state’e karşı alerjik ve paraya dokunan her şey konusunda yapısal olarak paranoyak.