04.02.2026

Outbox Pattern Nedir? Mikroservislerde Veri Kaybını Bitirin

Outbox Pattern ile DB + event yayınını atomik yapın. Kafka/RabbitMQ ile güvenilir entegrasyon, adım adım kurulum ve örnekler.

Outbox Pattern Nedir? Mikroservislerde Veri Kaybını Bitirin

Outbox Pattern, mikroservislerde veritabanına yazdığınız bir işlemi (ör. sipariş oluşturma) aynı anda mesaj kuyruğuna/event bus’a yayınlama ihtiyacınız olduğunda ortaya çıkan klasik sorunu çözer: “DB’ye yazıldı ama event gitmedi” ya da “Event gitti ama DB yazılamadı”.

Birçok ekip bu problemi ilk kez prod’da fark eder: Sipariş DB’de var ama stok servisi haberdar değil… ya da ödeme servisi event aldı ama sipariş aslında hiç oluşmamış. Bu yazıda Outbox Pattern’ı neden gerekli, nasıl kurulur, hangi araçlarla uygulanır ve hangi tuzaklara dikkat etmelisiniz sorularını net şekilde öğreneceksiniz.


Outbox Pattern Nedir? (Kısa Tanım)

Outbox Pattern, uygulamanın iş verisiyle birlikte aynı veritabanında bir outbox tablosuna “gönderilecek mesajı” yazması ve sonra ayrı bir süreçle bu mesajı Kafka/RabbitMQ/SQS gibi sistemlere güvenilir şekilde yayınlaması yaklaşımıdır.

Bu sayede:

  • DB transaction’ı içinde iş verisi + outbox kaydı birlikte commit olur.
  • Yayınlama daha sonra yapılır ama kayıp olmaz.
  • Sistem doğal olarak eventual consistency ile çalışır.

LSI anahtar kelimeler: transactional outbox, event publishing, message broker, mikroservis entegrasyonu, event-driven architecture, distributed transaction, CDC (Change Data Capture)


Neden Outbox Pattern Kullanmalıyım?

Dağıtık sistemlerde (mikroservis) “DB + mesajlaşma” ikilisini aynı anda atomik hale getirmek zordur.

Klasik anti-pattern: İki ayrı adım

  1. DB’ye yaz
  2. Mesajı broker’a yayınla

Arada hata olursa:

  • DB yazıldı, publish başarısız → downstream servisler habersiz
  • Publish oldu, DB rollback → hayalet event

“2PC (Two-Phase Commit) yapalım” neden kötü fikir olabilir?

2PC; karmaşıklık, performans, operasyonel yük ve broker desteği gibi nedenlerle modern mikroservis mimarilerinde çoğunlukla tercih edilmez.

Outbox Pattern, pratik ve kanıtlanmış bir çözüm sunar: tek gerçek kaynak DB transaction’ı olur.


Outbox Pattern Mimarisi: Nasıl Çalışır?

Aşağıdaki akış tipiktir:

  1. API isteği gelir (örn. POST /orders)
  2. Uygulama transaction açar
  3. orders tablosuna siparişi yazar
  4. outbox tablosuna yayınlanacak event’i yazar
  5. Transaction commit
  6. Ayrı bir publisher/worker outbox’tan kayıtları okur
  7. Mesaj broker’a yayınlar
  8. Başarıyla yayınlanırsa outbox kaydı işaretlenir (published) veya silinir

Basit tablo tasarımı

Alan Tip Amaç
id UUID Mesaj kimliği
aggregate_type text Order, Payment vb.
aggregate_id text/uuid İlgili kayıt
event_type text OrderCreated vb.
payload jsonb/text Mesaj içeriği
status text NEW / PUBLISHED / FAILED
created_at timestamp Sıralama/izleme
published_at timestamp Yayın zamanı

Adım Adım Uygulama (PostgreSQL + Node.js Örneği)

Aşağıdaki örnek, mantığı net görmek için minimal tutulmuştur.

1) Outbox tablosunu oluşturun

CREATE TABLE outbox (
  id uuid PRIMARY KEY,
  aggregate_type text NOT NULL,
  aggregate_id text NOT NULL,
  event_type text NOT NULL,
  payload jsonb NOT NULL,
  status text NOT NULL DEFAULT 'NEW',
  created_at timestamptz NOT NULL DEFAULT now(),
  published_at timestamptz
);

CREATE INDEX idx_outbox_status_created_at
  ON outbox (status, created_at);

2) İş kaydı + outbox kaydını aynı transaction’da yazın

// pseudo-code (Node.js + pg)
import { randomUUID } from "crypto";

async function createOrder(client, orderInput) {
  await client.query("BEGIN");
  try {
    const orderId = randomUUID();

    await client.query(
      "INSERT INTO orders(id, user_id, total) VALUES ($1,$2,$3)",
      [orderId, orderInput.userId, orderInput.total]
    );

    const outboxId = randomUUID();
    const event = {
      orderId,
      userId: orderInput.userId,
      total: orderInput.total,
      createdAt: new Date().toISOString()
    };

    await client.query(
      `INSERT INTO outbox(id, aggregate_type, aggregate_id, event_type, payload)
       VALUES ($1,$2,$3,$4,$5)`,
      [outboxId, "Order", orderId, "OrderCreated", event]
    );

    await client.query("COMMIT");
    return { orderId };
  } catch (e) {
    await client.query("ROLLBACK");
    throw e;
  }
}

3) Publisher (worker) ile outbox’tan publish edin

Bu worker periyodik çalışabilir (cron) veya sürekli döngüde olabilir.

// pseudo-code: Outbox publisher
async function publishOutboxBatch(client, broker, limit = 100) {
  // Aynı kaydı iki worker'ın almaması için FOR UPDATE SKIP LOCKED kullanın
  const res = await client.query(
    `SELECT id, event_type, payload
     FROM outbox
     WHERE status = 'NEW'
     ORDER BY created_at
     LIMIT $1
     FOR UPDATE SKIP LOCKED`,
    [limit]
  );

  for (const row of res.rows) {
    try {
      await broker.publish(row.event_type, row.payload); // Kafka/RabbitMQ adapter

      await client.query(
        `UPDATE outbox
         SET status = 'PUBLISHED', published_at = now()
         WHERE id = $1`,
        [row.id]
      );
    } catch (e) {
      await client.query(
        `UPDATE outbox
         SET status = 'FAILED'
         WHERE id = $1`,
        [row.id]
      );
    }
  }
}

Neden FOR UPDATE SKIP LOCKED?

  • Birden fazla worker çalıştırdığınızda aynı outbox kaydını iki kere publish etme riskini azaltır.

Outbox Pattern ile “En Az Bir Kez” Yayın ve İdempotency

Outbox Pattern çoğu senaryoda at-least-once delivery sağlar. Yani bazı edge-case’lerde aynı event iki kez yayınlanabilir.

Bu yüzden tüketici tarafında (consumer) şunu hedefleyin:

  • Idempotent consumer (aynı event iki kez gelse de sonuç değişmesin)

Pratik öneri

Event’e id (UUID) koyun ve consumer tarafında processed_events gibi bir tabloyla işlenmiş id’leri tutun.

Yaklaşım Artı Eksi
Idempotent consumer + event id Sağlam ve yaygın Ek tablo/okuma-yazma
Broker exactly-once Teoride güzel Operasyon/konfig zor, her yerde yok

CDC Tabanlı Outbox: Debezium ile Otomatik Publish

Outbox’u publish etmek için iki ana yöntem var:

1) Polling Publisher (uygulama/worker okur)

  • Basit, hızlı başlarsınız
  • Uygulama kodu artar
  • Polling yükü iyi ayarlanmalı

2) CDC (Change Data Capture) + Debezium

  • DB’de outbox tablosuna yazarsınız
  • Debezium, WAL/binlog’dan değişiklikleri yakalar
  • Kafka’ya otomatik taşır

Ne zaman CDC mantıklı?

  • Yüksek trafik, çok servis, daha az custom publisher kodu istediğinizde
  • Kafka ekosistemi zaten varsa

Gerçek Hayat Senaryosu: E-ticarette Sipariş Oluşturma

Problem: Sipariş oluşunca stok düşmeli, fatura kesilmeli, kargo süreci başlamalı.

Outbox’sız:

  • Sipariş DB’de var ama “OrderCreated” event’i publish edilemedi → stok düşmez → oversell.

Outbox ile:

  • Sipariş ve outbox kaydı aynı anda commit.
  • Broker geçici olarak down olsa bile event outbox’ta bekler.
  • Broker düzelince worker publish eder.

Bunu neden yapmalıyım? Çünkü işiniz büyüdükçe “nadiren olan” entegrasyon hataları bile müşteri kaybına ve maliyetli manuel düzeltmelere dönüşür.


Sık Yapılan Hatalar ve İpuçları

  • Outbox tablosunu şişirmek: PUBLISHED kayıtları periyodik arşivleyin/silin.
  • Sıralama garantisi beklemek: Tek bir aggregate (örn. tek orderId) için sıralama gerekiyorsa tasarımı buna göre yapın.
  • Retry stratejisi yok: FAILED için exponential backoff ve tekrar deneme tasarlayın.
  • Payload versiyonlamamak: Event şemalarını versiyonlayın (örn. OrderCreated.v1).

FAQ (Sık Sorulan Sorular)

1) Outbox Pattern ile exactly-once garanti eder miyim?

Genelde hayır. Tipik yaklaşım at-least-once + idempotent consumer kombinasyonudur.

2) Outbox tablosu performansı düşürür mü?

Doğru index ve batch publish ile genelde yönetilebilir. Yüksek trafikte CDC (Debezium) daha ölçeklenebilir olabilir.

3) RabbitMQ mu Kafka mı daha uygun?

İkisi de olur. Kafka event streaming için güçlüdür; RabbitMQ iş kuyruğu (task) ve routing’te pratik olabilir. Mimarinize göre seçin.

4) Outbox’ı her servis için mi kurmalıyım?

Event publish eden servislerde evet. Sadece senkron REST ile yaşayan serviste gerekmeyebilir.


Sonuç

Outbox Pattern, mikroservislerde veri tutarlılığı ve güvenilir event yayınlama için en pratik desenlerden biridir. DB transaction’ı içinde outbox kaydı yazarak “DB’ye yazıldı ama event gitmedi” sınıfı hataları büyük ölçüde ortadan kaldırırsınız.

Bir sonraki adım olarak:

  • Kendi projenizde küçük bir akış seçin (örn. UserCreated veya OrderCreated),
  • Outbox tablosunu ekleyin,
  • Basit bir publisher ile publish etmeyi deneyin.

Deneyiminizi ve karşılaştığınız edge-case’leri yorumlarda paylaşın; birlikte en iyi retry/temizlik stratejisini netleştirelim.