Fintech'te Go — Staff Engineer Seviyesinde Derinlemesine İnceleme

25 Ağustos 2026 · netologist · 60 dakika, 12746 kelime ·

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

  1. Büyük Resim: Go Fintech’te Nerede Kullanılır
  2. Referans Mimari
  3. Fintech Bağlamında Concurrency
  4. Idiomatic Go
  5. Go Design Pattern’leri
  6. Architecture Pattern’leri
  7. Fintech için Domain-Driven Design
  8. Payment Service Tasarımı
  9. Idempotency
  10. Ledger ve Double-Entry Accounting
  11. Database Tasarımı
  12. Transaction Pattern’leri (Outbox, Saga)
  13. Kafka ve Event-Driven Architecture
  14. Distributed Systems Temelleri
  15. Go Concurrency Primitive’leri Özeti
  16. API Tasarımı
  17. gRPC
  18. Security
  19. Fraud ve Risk
  20. Reconciliation ve Settlement
  21. Observability
  22. Testing
  23. Production Proje Yapısı
  24. Go Anti-Pattern’leri
  25. Performance
  26. Scalability
  27. Tam Örnek: Payment & Ledger Platform
  28. Production-Quality Kod Örneği
  29. Trade-off Analizi
  30. Öğrenme Roadmap’i
  31. 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)

Payment Processing / Payment Gateway / Orchestration

Card Processing

Bank Transfer / Money Movement / Wallet

Ledger Sistemleri

Trading / Brokerage

Crypto / Blockchain Altyapısı

Fraud Detection / Risk Engine

KYC / AML

Reconciliation / Settlement

Financial Data Processing / Event Processing / Real-Time Sistemler

API Gateway / Microservices / Internal Altyapı

Notification Sistemleri

Özet tablo:

DomainGo uyumuYaygın alternatif
Payment orchestrationMükemmelKotlin/Java (takım legacy’si)
Card auth hot pathÇok iyiC++/Java-GraalVM (aşırı tail latency)
LedgerMükemmelJava (büyük bankalar), Rust (bazı yeni oyuncular)
Matching engineBir katman üstünde iyiC++ (engine’in kendisi)
Fraud kural orkestrasyonuMükemmel
ML scoring/eğitimZayıf uyumPython
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/gatewayMü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

Payment Service

Account Service

Ledger Service

Fraud Service / Risk Service

Payment Provider (entegrasyon katmanı)

Notification Service

Reconciliation Service


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


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)

type BaseHandler struct {
    Logger *slog.Logger
}

type PaymentHandler struct {
    BaseHandler
    svc PaymentService
}

Küçük interface’ler

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

func NewLedgerService(db *sql.DB, publisher EventPublisher) *LedgerService { ... }

Implicit interface’ler

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

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

Pointer vs value receiver’lar

Package visibility / internal/ paketleri

Domain’e göre paketleme (katmana göre değil)

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


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

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)
    }
}

Builder

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
}

Structural (Yapısal)

Adapter

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
}

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))

Facade

Proxy

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 }
}

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)
}

Command

Chain of Responsibility

type Middleware func(http.Handler) http.Handler

handler = LoggingMiddleware(AuthMiddleware(RateLimitMiddleware(paymentHandler)))

Observer


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.

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:


7. Fintech için Domain-Driven Design

Fintech örnekleriyle yapı taşları

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
}

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:

  1. Bir provider status-check API çağrısı (provider, idempotency key’inize göre sorgulamayı destekliyorsa GET /charges/{id}), veya
  2. Provider’ın webhook’unu beklemek, veya
  3. 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. 1. istek: mevcut key bulunmaz → status=IN_PROGRESS ve request body’nin bir hash’i ile bir satır eklenir → payment işlenir → satır status=COMPLETED ve response body ile güncellenir.
  2. 2.-5. istekler (1. hâlâ işlenirken): insert denemesi unique constraint’e çarpar (key üzerinde) → çakışma → handler mevcut satırı okur; eğer status=IN_PROGRESS ise, latency bütçenize bağlı olarak kısaca bloklayabilir/poll edebilir veya 409 Processing döndürebilir.
  3. Tamamlandıktan sonraki istekler: handler status=COMPLETED bulur, 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?

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

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

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.

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)
}
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

15. Go Concurrency Primitive’leri Özeti

PrimitiveAmaçArtıEksiFintech use case
goroutinehafif eşzamanlı fonksiyonucuz, basitsınırlandırılmaz/iptal edilmezse sızargelen istek başına bir tane
channelgoroutine’ler arası iletişimgüvenli devir, doğal pipeline’laryanlış kullanılırsa deadlock olabilirworker pool task dağıtımı
sync.Mutexkarşılıklı dışlamabasit, hızlıyüksek yükte contentionmerchant config’lerinin memory-içi cache’ini korumak
sync.RWMutexçoklu okuyucu / tek yazarokuma-ağırlıklı state için iyiwriter starvation mümkünhot-reload edilen config/feature flag’ler
sync/atomiclock-free sayaçlarçok hızlıkarmaşık state için yanlış kullanılması kolayistek sayaçları, circuit-breaker hata sayıları
sync.Oncetam olarak bir kez çalıştırbasit init pattern’itekrarlanan reset’ler için değiltek seferlik client/config initialization
sync.WaitGroupN goroutine’i beklebasit fan-out/joinhata propagasyonu yokN notification göndermek, bireysel hatalar önemli değil
errgroup.Grouphata + cancellation ile fan-outilk-hata propagasyonu, ctx cancelexternal dependency (golang.org/x/sync)eşzamanlı fraud+risk çağrıları (§3)
context.Contextcancellation/deadline/değerlerstandart, compose edilebilirher yere thread edilmelirequest yolundaki her I/O çağrısı
worker poolsınırlı eşzamanlı işlemekaynak kullanımını kontrol edernaif go fn()‘den daha fazla kodreconciliation batch işleme
semaphore (chan struct{})concurrency’yi sınırlaimplement etmesi basitmanuel bookkeepingeş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 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=...
{
  "type": "insufficient_funds",
  "title": "Insufficient funds",
  "status": 422,
  "detail": "Account acc_1 has insufficient available balance.",
  "payment_id": "pay_123"
}

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/JSONgRPC
External/partner-facing API’ler✅ standart, evrensel toolingnadiren, partner talep etmediği sürece
Internal servis-serviskullanı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 disiplinidaha gevşek (JSON izin vericidir)✅ protobuf field numaralandırma kurallarıyla zorunlu kılınır
Debuggability✅ curl edilebilir, insan-okunabilirgrpcurl/tooling gerektirir

Çoğu fintech şirketi: edge’de (partner/client-facing) REST/JSON, Go servisleri arasında internal olarak gRPC.


18. Security

Compliance’ın mimariye etkisi


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)

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:

  1. Asla sessizce otomatik düzeltmeyin. Discrepancy’yi tam context ile (her iki tarafta da transaction ID’leriyle) loglayın.
  2. 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).
  3. 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.
  4. Geri kalanını tam bir audit trail ile manuel incelemeye eskale edin.
  5. Düzeltme kayıtları, orijinal transaction’a referans veren yeni ledger entry’leridir (asla geçmişi düzenlemez), append-only garantisini (§10) korur.

21. Observability

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),
)

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

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)
            }
        })
    }
}

Para transferleri için test edilmesi gereken edge case’ler


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:

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ü:

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-patternKötüİyi
OverengineeringTek provider’lı bir servis için 5 abstraction katmanlı bir plugin mimarisi kurmakAbstraction’ı ikinci provider’ı entegre ederken ekleyin, öncesinde değil
Devasa interface’lertype Repository interface { /* 20 method */ }PaymentReader, PaymentWriter‘a bölün, her biri 1-3 method’lu
Her yerde interfaceHiç 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ı abstractionBir PaymentStrategyFactoryProviderResolverBir switch ifadesi veya bir map — Go doğrudanlığı ödüllendirir
Derin paket hiyerarşisiinternal/domain/payment/entities/aggregates/paymentinternal/payment
Global stateHer yerden erişilen var GlobalDB *sql.DBServis başına constructor-injected *sql.DB
Goroutine leak’lerigo func() { <-neverClosedChannel }()Her zaman ctx.Done() üzerinde select yapın veya iptal edilebilir bir channel kullanın
Yok sayılan hatalarBir 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 saklamakContext 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 kullanmakBir 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 singletonConstructor’lar aracılığıyla açık construction ve dependency injection
Generic utility paketleriÇöp kutusu haline gelen bir utils veya common paketiHelper’ları sahibi olan domain paketine koyun; sadece gerçekten paylaşıldığında pkg/‘e çıkarın
God servislerPayment’lar, iade’ler, disputes ve settlement’a yayılan 40 method’lu tek bir PaymentServiceGerçek alt-domain’lere göre bölün: PaymentService, RefundService, DisputeService
God struct’larOlası 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

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.


26. Scalability

100 req/s → 10.000 req/s → 100.000 req/s

Kollar, kabaca fintech takımlarının onlara ulaşma sırasına göre:

  1. Orkestrasyon servislerinin stateless yatay ölçeklenmesi (ucuz, önce bunu yapın).
  2. Okuma-ağırlıklı, kritik-olmayan-yol verileri için caching (Redis).
  3. Database için read replica’lar.
  4. Kritik consistency yolunda olmayan her şey için Kafka üzerinden asenkron işleme.
  5. Connection pooling ayarı (PgBouncer, dikkatli MaxOpenConns).
  6. 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    │
             └────────────┘                    └────────────────┘

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ı:


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

Seviye 2 — Idiomatic Go

Seviye 3 — Backend Go

Seviye 4 — PostgreSQL + Transaction’lar

Seviye 5 — Kafka + Event-Driven Sistemler

Seviye 6 — Distributed Systems

Seviye 7 — Fintech Domain

Seviye 8 — Production Architecture

Seviye 9 — Security ve Compliance

Seviye 10 — Staff/Principal-Seviyesi Mimari


31. Master Checklist

1. Bilmem gereken Go pattern’leri

2. Bilmem gereken distributed-system pattern’leri

3. Bilmem gereken fintech pattern’leri

4. Bilmem gereken teknolojiler

5. Bilmem gereken Go idiom’ları

6. Bilmem gereken database konuları

7. Bilmem gereken Kafka/event-driven konuları

8. Bilmem gereken security/compliance konuları

9. Portfolio için yapmam gereken projeler

10. Senior → Staff’a geçmek için öğrenmem gereken konular


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.