Yazılarımız

Veri Akademi

MİCROSERVİCES’TE SAGA VE OUTBOX PATTERN: TUTARLILIK VE MESAJLAŞMA STRATEJİLERİ

Microservices mimarisine geçtiğinizde işler genellikle “bağımsız deploy” ve “ekiplerin otonomisi” ile başlar; ilk büyük sürtünme ise tutarlılık ve mesajlaşma katmanında çıkar. Tek bir veritabanında, tek bir transaction içinde çözdüğünüz sipariş oluşturma, stok düşme ve ödeme alma gibi akışlar, servisler ayrıştığında dağıtık bir probleme dönüşür. Üstelik sorun sadece teknik değil; ürün davranışını, hata anındaki kullanıcı deneyimini ve operasyon ekibinin görünürlüğünü de doğrudan etkiler.

Bu noktada iki güçlü yaklaşım öne çıkar: Saga ve Outbox. Saga, uzun süreli iş akışlarını adımlara bölerek, her adımın başarısına göre ilerler ve gerekirse telafi (compensation) işlemleriyle geri alır. Outbox ise veritabanı transaction’ı ile mesaj yayınını “aynı kaderi paylaşacak” şekilde bağlayarak, kaybolan mesajlar ve çift yayın gibi riskleri azaltır.

Bu yazıda, microservices’te tutarlılık ve güvenilir mesajlaşma hedefiyle Saga ve Outbox pattern’lerini birlikte ele alacağız. “Hangi problemi çözüyorlar?”, “Ne zaman hangisini seçmeliyim?”, “Kafka/RabbitMQ gibi broker’larla nasıl uygulanır?” ve “Operasyonel olarak nasıl sürdürülebilir hale gelir?” sorularını gerçekçi Java/Spring örnekleriyle cevaplayacağız.

Servisler arası sipariş akışı için olay temelli tutarlılık yaklaşımının uçtan uca özetlenmesi

Dağıtık Tutarlılık Problemi: Neden “Tek Transaction” Yetmiyor?

Monolitik bir sistemde sipariş oluşturma gibi bir akış, çoğu zaman tek bir veritabanı transaction’ı ile tamamlanır: sipariş kaydı açılır, stok düşülür, ödeme provizyonu alınır ve her şey commit edilir. Microservices’te ise her servis kendi verisini yönetir; dolayısıyla tek bir veritabanı transaction’ı artık mümkün değildir. Bir servis commit ederken diğer servis hata alabilir; ağ gecikmesi, broker kesintisi veya geçici bir kilitlenme tüm akışı yarıda bırakabilir.

Bu tür senaryolarda temel hedef genellikle eventual consistency olur: sistem, kısa süreli tutarsızlıklara izin verir ama sonunda doğru duruma ulaşır. Buradaki kritik detay şudur: “Sonunda” kelimesi, tasarlanmış bir iyileşme mekanizması olmadan gerçekleşmez. Bu iyileşmeyi sağlayan mekanizma çoğu zaman ya Saga’dır ya da sağlam bir mesaj yayın stratejisidir; idealde ikisi birlikte çalışır.

2PC Neden Pratikte Zor?

Teorik olarak iki-aşamalı commit (2PC) dağıtık transaction için bir çözüm gibi görünür; ancak microservices dünyasında ölçeklenebilirlik ve bağımsızlık hedefleriyle çelişir. Koordinatör bağımlılığı, bloklayıcı yapısı ve hata durumlarında “kilitli bekleme” riskleri, modern mimarilerde 2PC’yi çoğunlukla pratik dışı kılar. Ayrıca farklı veri depoları (SQL, NoSQL) ve farklı hizmet sınırları devreye girdiğinde, standart bir 2PC uygulaması gerçekçi değildir.

Eventual Consistency ve İş Kuralları

Eventual consistency’yi benimsemek, iş kuralını daha açık tasarlamayı gerektirir: “Ödeme alınamazsa sipariş iptal mi edilir?”, “Stok kısa süreli eksiye düşebilir mi?”, “Kullanıcıya hangi aşamada ne gösterilir?” gibi sorular teknik karardan önce gelir. Saga ile iş adımlarını ve telafi stratejisini netleştirir; Outbox ile bu adımların servisler arası güvenle taşınmasını sağlarsınız.

Saga Pattern: Uzun Süreli İş Akışlarını Yönetmek

Saga pattern, uzun süren bir iş sürecini küçük, atomik adımlara böler. Her adım kendi yerel transaction’ını yönetir. Bir adım başarısız olursa, daha önce tamamlanan adımlar için tanımlanmış telafi işlemleri tetiklenir. Böylece “tam geri alma” çoğu zaman mümkün olmasa bile, sistem iş açısından kabul edilebilir bir sona taşınır.

Orkestrasyon: Merkezi Süreç Yöneticisi

Orkestrasyon yaklaşımında bir “saga orchestrator” (süreç yöneticisi) vardır. Bu bileşen akışı yönetir: hangi servis çağrılacak, hangi event bekleniyor, timeout olursa ne yapılacak gibi kararlar tek bir yerde toplanır. Büyük avantajı, süreç kontrolünün görünür ve yönetilebilir olmasıdır. Dezavantajı ise orchestrator’ın iyi tasarlanmazsa bir “mini monolit”e dönüşebilmesidir.

Koreografi: Olaylarla Dağıtık Akış

Koreografi yaklaşımında merkezi yönetici yoktur. Servisler domain event yayınlar; diğer servisler bu event’lere tepki verir. Akış, event zinciriyle ilerler. Bu yaklaşım servis bağımsızlığını artırır; ancak akışın bütününü izlemek ve hata senaryolarını yönetmek daha zor olabilir. Özellikle çok adımlı süreçlerde “hangi event zincirinin nerede koptuğunu” görmek için güçlü izlenebilirlik gerekir.

  • Orkestrasyon: Süreç kontrolü net, hata yönetimi merkezi; ancak koordinatör bağımlılığı artabilir.
  • Koreografi: Servis otonomisi güçlü, gevşek bağlılık yüksek; ancak uçtan uca izlenebilirlik ve test yükü artar.
  • Hibrit: Kritik akışlar orkestrasyonla, daha basit reaksiyonlar koreografiyle ilerletilebilir.
Orkestrasyonlu saga adımlarında durum geçişlerinin, başarı ve telafi akışının kısa bir süreç özeti

Saga Tasarımında Telafi (Compensation) ve İdempotency

Saga’nın başarısı, telafi işlemlerinin doğru tanımlanmasına bağlıdır. Telafi, “tam olarak geri alma” değildir; iş açısından kabul edilebilir bir karşı hamledir. Örneğin “kargo planlandı” adımını telafi etmek “kargoyu iptal et” olabilir; ama kargo artık yola çıktıysa telafi “iade süreci başlat” şeklinde farklılaşır. Bu yüzden telafi işlemlerini yazarken teknik simetriye değil, iş gerçekliğine odaklanmak gerekir.

Telafi İşlemleri Ne Zaman Çalışır?

Telafi, genellikle bir adım başarısız olduğunda veya belirli bir süre içinde beklenen event gelmediğinde devreye girer. Timeout’lar bu noktada kritiktir: gereğinden kısa timeout, gereksiz telafi tetikler; gereğinden uzun timeout ise kullanıcı deneyimini bozar ve kapasiteyi tüketir. Uygulamada, her adım için ayrı timeout ve tekrar deneme stratejisi tanımlamak daha sağlıklıdır.

Idempotency: Dağıtık Dünyanın Sigortası

Mesajlaşma dünyasında “en az bir kez teslim” (at-least-once) yaygındır. Bu da aynı event’in birden fazla kez işlenebileceği anlamına gelir. Saga adımlarında idempotency bu yüzden zorunludur. Örneğin “stok düş” komutu iki kez gelirse stok iki kez düşmemelidir; bunun için bir deduplication anahtarı, işleme kaydı veya benzersiz kısıt gerekir. Idempotency’yi yalnızca tüketicide değil, mümkünse servis sınırında (API) da uygulamak hatayı daha erken yakalar.

Outbox Pattern: Transactional Mesaj Yayını ile Kayıp Mesaj Riskini Azaltmak

Outbox pattern’in çıkış noktası basittir: Bir servis hem kendi verisini güncelliyor hem de bir event yayınlıyorsa, bu iki işi ayrı sistemlere (DB ve broker) yazarken tutarlılık sorunu yaşayabilirsiniz. DB commit olur ama broker’a yayın gidemez; ya da yayın gider ama DB commit olmaz. Sonuçta ya “hayalet event” ya da “kayıp event” oluşur. Outbox, bu ikili yazmayı tek transaction’a yaklaştırır.

Transactional Outbox Tablosu

Yaklaşım şöyledir: Servis, domain değişikliğini kendi tablolarına yazarken aynı transaction içinde bir outbox tablosuna da event kaydını ekler. Böylece “DB commit olduysa event kaydı da oluştu” garantisi elde edilir. Sonrasında ayrı bir yayınlayıcı (poller veya CDC) outbox tablosunu okuyup broker’a yayın yapar. Yayın başarılı olunca outbox kaydı işaretlenir veya silinir.

Poller mı CDC mi?

Outbox’tan broker’a taşıma için iki popüler yol vardır. Poller, belirli aralıklarla outbox tablosunu tarar; basittir ama yüksek trafikte dikkatli indeksleme ve batch yönetimi ister. CDC (Change Data Capture) ise veritabanındaki değişiklikleri (ör. WAL/redo log) okuyup event akışına dönüştürür; daha az sorgu yüküyle daha gerçek zamanlı çalışabilir, fakat kurulum ve operasyon karmaşıklığı daha yüksektir. Hangi yolun seçileceği ekip olgunluğu, veri tabanı ve latency hedeflerine göre değişir.

/**
 * Basit bir transactional outbox modeli (Spring/JPA örneği)
 * Amaç: domain değişikliği ile event kaydını aynı transaction içinde yazmak.
 */
@Entity
@Table(name = "outbox_event")
public class OutboxEvent {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false, unique = true, length = 80)
  private String eventId; // dedup için benzersiz

  @Column(nullable = false, length = 80)
  private String aggregateId;

  @Column(nullable = false, length = 60)
  private String eventType;

  @Column(nullable = false, columnDefinition = "TEXT")
  private String payloadJson;

  @Column(nullable = false)
  private Instant createdAt;

  @Column(nullable = false)
  private boolean published;

  // getters/setters ...
}

// Domain işlemi içinde kullanım (pseudo)
@Transactional
public void createOrder(CreateOrderCommand cmd) {
  Order order = orderRepository.save(Order.create(cmd));

  String eventId = UUID.randomUUID().toString();
  OutboxEvent evt = new OutboxEvent();
  evt.setEventId(eventId);
  evt.setAggregateId(order.getId().toString());
  evt.setEventType("OrderCreated");
  evt.setPayloadJson(toJson(new OrderCreated(order.getId(), cmd.getUserId(), cmd.getTotal())));
  evt.setCreatedAt(Instant.now());
  evt.setPublished(false);

  outboxRepository.save(evt);
}

Saga + Outbox Birlikte: Tutarlılık ve Mesajlaşma Stratejilerini Birleştirmek

Saga size iş akışının “ne zaman, hangi adımlarla, nasıl telafi edileceğini” verir. Outbox ise bu akıştaki event’lerin güvenilir şekilde taşınmasına yardım eder. Bu ikisi birlikte kullanıldığında, özellikle “sipariş-ödeme-stok” gibi çok servisli süreçlerde kaybolan mesajlar, yarıda kalan süreçler ve tekrar işleme hataları ciddi ölçüde azalır.

“Exactly-once” Miti ve Pratik Yaklaşım

Gerçek dünyada uçtan uca “exactly-once” garantisi çoğu sistemde ya çok pahalıdır ya da illüzyondur. Daha sürdürülebilir yaklaşım şudur: at-least-once mesajlaşma + idempotent tüketim + deduplication + görünür süreç durumu. Outbox, üretici tarafında “yayınla/commit et” yarışını azaltır; saga state yönetimi ise süreç seviyesinde tekrarları güvenli hale getirir.

Event Versiyonlama ve Şema Disiplini

Event’ler uzun ömürlü kontratlardır. Saga akışı event’lere bağlıysa, event şemasını değiştirmek doğrudan iş akışını etkiler. Bu yüzden event versiyonlama, geriye uyumluluk ve tüketici tarafında toleranslı parse stratejisi kritik hale gelir. Basit bir kural olarak: alan eklemek genellikle güvenlidir; alan silmek veya anlamını değiştirmek tehlikelidir. Şema disiplini, “bugün çalışıyor” seviyesinden “yarın da çalışacak” seviyesine geçişi sağlar.

Outbox tablosundan broker’a iletimde tekrar deneme ve işaretleme yaklaşımının dayanıklı akış düzeni

Uygulama Yaklaşımları: Java, Spring, Kafka ile Gerçekçi Akış

Java/Spring ekosisteminde Saga ve Outbox için yaygın bir yaklaşım, domain event üretimini uygulama servisinde standardize etmek ve yayın katmanını ayrı bir bileşen haline getirmektir. Kafka gibi bir broker kullanıyorsanız, producer tarafında anahtar seçimi (partitioning), retry politikaları ve idempotent producer seçenekleri (varsa) performans ve sırayı etkiler. Tüketici tarafında ise offset yönetimi, yeniden deneme ve dead-letter stratejisi işin kalbidir.

Retry, Backoff ve Dead-Letter Stratejisi

Dağıtık sistemler geçici hatalarla yaşar. Bu yüzden “retry yoksa dayanıklılık yoktur” denebilir. Ancak kontrolsüz retry, kaskad arızalara yol açar. En iyi pratik, exponential backoff ile sınırlı retry ve sonunda dead-letter kuyruğuna (DLQ) düşürmektir. DLQ, sadece hata çöplüğü değil, süreç iyileştirme için bir geri bildirim kaynağıdır: hangi event’ler, hangi nedenlerle, hangi sıklıkla düşüyor?

Observability: Trace, Log ve Süreç Durumu

Saga orkestrasyonunda sürecin hangi adımda olduğunu, hangi telafinin çalıştığını, hangi event’in beklediğini görmek zorundasınız. Bu, sadece log ile çözülmez. Correlation id, distributed tracing (ör. traceparent), metrikler ve saga state tablosu birlikte düşünülmelidir. “Sipariş 10 dakikadır bekliyor” sorusuna tek komutla cevap verebilmek, operasyon maliyetini dramatik şekilde düşürür.

/**
 * Basitleştirilmiş saga orchestrator örneği (durum makinesi yaklaşımı)
 * Not: Bu örnek konsepti göstermek içindir; production'da daha güçlü hata/timeout yönetimi gerekir.
 */
public class OrderSagaOrchestrator {

  private final SagaStateRepository sagaStateRepository;
  private final CommandGateway commandGateway;

  public OrderSagaOrchestrator(SagaStateRepository repo, CommandGateway gateway) {
    this.sagaStateRepository = repo;
    this.commandGateway = gateway;
  }

  public void onOrderCreated(OrderCreated evt) {
    SagaState state = sagaStateRepository.createNew(evt.orderId(), "STARTED", evt.eventId());

    // 1) Stok ayır
    commandGateway.send(new ReserveStockCommand(evt.orderId(), evt.items(), state.correlationId()));
    sagaStateRepository.updateStep(state.id(), "STOCK_REQUESTED");
  }

  public void onStockReserved(StockReserved evt) {
    SagaState state = sagaStateRepository.findByCorrelationId(evt.correlationId());
    if (state.isProcessed(evt.eventId())) return; // idempotency (basit)

    // 2) Ödeme al
    commandGateway.send(new CapturePaymentCommand(state.orderId(), state.totalAmount(), state.correlationId()));
    sagaStateRepository.updateStep(state.id(), "PAYMENT_REQUESTED");
    sagaStateRepository.markProcessed(state.id(), evt.eventId());
  }

  public void onPaymentCaptured(PaymentCaptured evt) {
    SagaState state = sagaStateRepository.findByCorrelationId(evt.correlationId());
    if (state.isProcessed(evt.eventId())) return;

    // 3) Siparişi onayla
    commandGateway.send(new ApproveOrderCommand(state.orderId(), state.correlationId()));
    sagaStateRepository.updateStep(state.id(), "COMPLETED");
    sagaStateRepository.markProcessed(state.id(), evt.eventId());
  }

  public void onPaymentFailed(PaymentFailed evt) {
    SagaState state = sagaStateRepository.findByCorrelationId(evt.correlationId());
    if (state.isProcessed(evt.eventId())) return;

    // Telafi: Stok ayırma geri alınır, sipariş iptal edilir
    commandGateway.send(new ReleaseStockCommand(state.orderId(), state.correlationId()));
    commandGateway.send(new CancelOrderCommand(state.orderId(), state.correlationId()));
    sagaStateRepository.updateStep(state.id(), "COMPENSATED");
    sagaStateRepository.markProcessed(state.id(), evt.eventId());
  }
}

Test ve Operasyon: “Çalışıyor”dan “Sürdürülebilir”e Geçiş

Saga ve Outbox uygulamak, sadece kod yazmakla bitmez. Dağıtık hatalar çoğu zaman üretimde ortaya çıkar; bu yüzden test ve operasyon disiplinini mimarinin parçası haline getirmek gerekir. Aksi halde “zaman zaman kaybolan sipariş” gibi görünmesi zor sorunlar uzun süre gizli kalır.

Contract Test ve Event Akışı Doğrulama

Event kontratlarını doğrulamak için contract test yaklaşımı etkilidir. Üretici servis, yayınladığı event şemasını sürümleyerek paylaşır; tüketici servisler kendi beklentilerini test eder. Bu sayede şema değişikliği, deploy sonrası sürpriz bir kırılma yerine CI aşamasında yakalanır. Ayrıca, “aynı event’i iki kez tüketirsem ne olur?” testleri idempotency için vazgeçilmezdir.

Failure Drill: Planlı Hata Tatbikatı

Broker geçici olarak kapalıyken, veritabanı yavaşlarken veya tek bir consumer instance düştüğünde sistemin nasıl davrandığını önceden görmek gerekir. Planlı hata tatbikatları, telafi adımlarının gerçekten çalışıp çalışmadığını, retry politikalarının sistemi boğup boğmadığını ve operasyon ekibinin hangi sinyalleri izlemesi gerektiğini netleştirir. Bu pratik, “krizde öğrenme” maliyetini düşürür.

Hangi Durumda Hangisini Seçmeliyim?

Birçok ekip Saga ve Outbox’ı “ya o ya bu” gibi düşünür; oysa çoğu senaryoda cevap “ikisi birlikte”dir. Saga, iş sürecinin tutarlılık modelini kurar. Outbox, bu modelin event’lerle güvenilir şekilde yayılmasına destek olur. Yine de kullanım yoğunluğu farklı olabilir: Bazı akışlarda Saga kritik, Outbox standart bir yayın mekanizmasıdır; bazılarında ise Outbox olmazsa olmaz, Saga daha sınırlı bir kapsamda kalır.

Karar verirken şu sorular işinizi kolaylaştırır: Süreç kaç servis içeriyor? Telafi mümkün mü, yoksa alternatif bir iş akışı mı tasarlamak gerekiyor? Event kaybı veya tekrar işleme, kullanıcıya nasıl yansıyor? Gecikme toleransı nedir? Operasyon ekibi, süreç durumunu izleyebilecek mi? Bu soruların cevapları, teknik seçimi doğal olarak yönlendirir.

Derinleşmek İsterseniz: Uygulamalı Eğitim Yolu

Saga ve Outbox’ı sağlam şekilde hayata geçirmek; domain event tasarımı, transaction sınırları, idempotency, retry/DLQ, izlenebilirlik ve test stratejilerini birlikte ele almayı gerektirir. Eğer bu konuları Java ekosisteminde uçtan uca, uygulama odaklı bir yaklaşımla pekiştirmek isterseniz Java Microservices Eğitimi içeriğinde benzer senaryoları gerçekçi örneklerle ele alıyoruz.

En iyi sonuç için küçük başlayın: tek bir kritik akış seçin, saga adımlarını ve telafi stratejisini netleştirin, outbox ile güvenilir yayın hattı kurun, ardından observability ve test disiplinini ekleyin. Bu yaklaşım, mimariyi bir anda “mükemmel” yapmaya çalışmadan, kontrollü şekilde olgunlaştırmanızı sağlar.

 VERİ AKADEMİ