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
- DB’ye yaz
- 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:
- API isteği gelir (örn.
POST /orders) - Uygulama transaction açar
orderstablosuna siparişi yazaroutboxtablosuna yayınlanacak event’i yazar- Transaction commit
- Ayrı bir publisher/worker outbox’tan kayıtları okur
- Mesaj broker’a yayınlar
- 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.
UserCreatedveyaOrderCreated), - 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.