İçeriğe atla

Transactional Outbox Pattern: Dağıtık Sistemlerde Güvenilir Event Publishing

Transactional Outbox Pattern'in dağıtık sistemlerdeki dual-write problemini nasıl çözdüğünü, PostgreSQL, DynamoDB ve CDC araçlarıyla pratik implementasyonlarını öğren.

Ayhan Sipahi Ayhan Sipahi

Her event-driven servis er ya da geç aynı anda iki şey yapmak zorunda kalıyor: kendi state’ini değiştirmek ve bunu sistemin geri kalanına duyurmak. Distributed transaction olmadan database write’ı ile broker publish’i atomik hale getirilemiyor; biri başarısız olurken diğeri başarılı olabiliyor. Geriye ya stoğu rezerve edilmemiş bir sipariş kalıyor ya da var olmayan bir sipariş için rezerve edilmiş stok.

Transactional Outbox Pattern ikinci write’ı ortadan kaldırıyor. Event, business data ile aynı transaction içinde aynı database’deki bir outbox table’a yazılıyor; ayrı bir relay bu tabloyu okuyup publish ediyor. Polling relay ile başla: yeni bir altyapı gerektirmiyor ve davranışını SQL ile takip etmek kolay. Poll latency’si ya da database yükü sınırlayıcı olmaya başladığında Change Data Capture’a veya yönetilen bir stream-to-bus pipe’a geç. Üç relay de at-least-once teslimat yapıyor, yani consumer tarafında idempotency her durumda gerekiyor.

Dual-Write Problemi#

Yaygın bir senaryo: bir order servisi, siparişi database’e kaydetmeli ve OrderCreated eventi yayınlamalı. Basit yaklaşım şöyle görünüyor:

async function createOrder(orderData: Order) {
  // Adım 1: Database'e kaydet
  await db('orders').insert(orderData);

  // Adım 2: Event yayınla
  await messageQueue.publish('OrderCreated', orderData);
}

Hasarın büyük kısmı iki hata modundan geliyor ve ikisi de sistemi tutarsız bırakıyor.

Hata Senaryosu 1: Database başarılı, event yayını başarısız

  • Message broker’a network timeout
  • Message broker geçici olarak down
  • Servisin database write’dan sonra crash olması
  • Sonuç: Sipariş database’de var ama inventory servisi eventi hiç almıyor. Stok rezerve edilmiyor.

Hata Senaryosu 2: Event yayını başarılı, database başarısız

  • Database write constraint ihlal ediyor
  • Deadlock nedeniyle transaction rollback
  • Database bağlantısı kopuyor
  • Sonuç: Inventory servisi eventi alıyor ve stok rezerve ediyor ama sipariş yok. Data tutarsızlığı.
InventoryServiceMessageBrokerDatabaseAPIInventoryServiceMessageBrokerDatabaseAPISiparis kaydedildiNetwork timeoutTUTARSIZ DURUMSiparis verisi varEvent hic almadiStok rezerve edilmedi1. INSERT order (BASARILI)2. OrderCreated yayinla (BASARISIZ)

Neden Two-Phase Commit (2PC) Kullanmıyoruz?#

Distributed transaction’lar teknik olarak database ile broker’ı tek bir işlemde birleştirebiliyor. Trade-off’lar 2PC’yi bu problem için pratik olmaktan çıkarıyor:

  • Performance overhead: Sistemler arası transaction koordinasyonu ciddi latency ekliyor
  • Availability düşüşü: Herhangi bir katılımcı down olursa tüm operasyon başarısız
  • Complexity: XA transaction’ları doğru implement etmek zor
  • Sınırlı destek: Birçok message broker 2PC desteklemiyor
  • Coupling: Microservices bağımsızlık prensiplerini ihlal ediyor

Distributed transaction’lardan kaçınmak, onları güvenilir çalıştırmaya çalışmaktan daha iyi bir yaklaşımdır.

Outbox Pattern: Temel Kavramlar#

Pattern, iki ayrı sisteme yazmak yerine (database + message broker), aynı database’deki iki tabloya tek bir ACID transaction içinde yazıyor.

Temel Bileşenler#

  1. Outbox Table: Yayınlanacak eventleri saklıyor, business datanla aynı database’de yaşıyor
  2. Business Transaction: Hem business tablolara hem outbox’a yazan tek ACID transaction
  3. Message Relay: Ayrı bir process outbox’u okuyor ve message broker’a publish ediyor
  4. Idempotent Consumer’lar: Downstream servisler duplicate eventleri doğru handle ediyor

Nasıl Çalışıyor#

Basarili

Basarisiz

API Request

DB Transaction Basla

Business Data Yaz

Event'i Outbox Table'a Yaz

Transaction Commit

Her Ikisi de Atomik Yazildi

Her Ikisi de Rollback

Message Relay Outbox Okuyor

Message Broker'a Publish

Event'i Published Olarak Isaretle

Downstream Servisler Tuketiyor

Commit başarılı olduktan sonra relay crash olabilir, retry yapabilir ya da geride kalabilir; correctness’i tehdit edemiyor. Yapabileceği en kötü şey geç ya da iki kez teslim etmek ve ikisi de telafi edilebilir.

Implementasyon Yaklaşımı 1: Polling Publisher#

En basit yaklaşım outbox table’ı periyodik olarak poll ediyor. Pratikte işe yarayan şey:

Temel Implementasyon#

-- Outbox table schema (PostgreSQL)
CREATE TABLE outbox (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  aggregate_type VARCHAR(100) NOT NULL,
  aggregate_id VARCHAR(100) NOT NULL,
  event_type VARCHAR(100) NOT NULL,
  payload JSONB NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  published BOOLEAN DEFAULT FALSE
);

-- Verimli polling için kritik index
CREATE INDEX idx_outbox_unpublished
ON outbox(created_at)
WHERE published = false;

Producer: Outbox’a Yaz#

async function createOrder(orderData: Order) {
  await db.transaction(async (trx) => {
    // Siparişi ekle
    const order = await trx('orders').insert({
      id: orderData.id,
      customer_id: orderData.customerId,
      total: orderData.total,
      status: 'PENDING'
    }).returning('*');

    // Eventi outbox'a ekle AYNI TRANSACTION İÇİNDE
    await trx('outbox').insert({
      id: uuid(),
      aggregate_type: 'Order',
      aggregate_id: order[0].id,
      event_type: 'OrderCreated',
      payload: {
        orderId: order[0].id,
        customerId: orderData.customerId,
        total: orderData.total,
        items: orderData.items
      },
      created_at: new Date()
    });

    // Her ikisi de başarılı veya her ikisi de başarısız - atomicity garanti
  });
}

Publisher: Poll ve Publish#

async function publishOutboxEvents() {
  // FOR UPDATE SKIP LOCKED kullanarak concurrent processing'i önle
  const events = await db.raw(`
    SELECT * FROM outbox
    WHERE published = false
    ORDER BY created_at
    LIMIT 100
    FOR UPDATE SKIP LOCKED
  `);

  for (const event of events.rows) {
    try {
      // Message broker'a publish et
      await messageQueue.publish(event.event_type, {
        messageId: event.id,  // Deduplication için önemli
        aggregateId: event.aggregate_id,
        payload: event.payload
      });

      // Published olarak işaretle
      await db('outbox')
        .where('id', event.id)
        .update({ published: true });

    } catch (error) {
      console.error('Event publish başarısız:', error);
      // Sonraki poll'da retry yapılacak - at-least-once delivery
    }
  }
}

// Publisher'ı her 5 saniyede bir çalıştır
setInterval(publishOutboxEvents, 5000);

FOR UPDATE SKIP LOCKED clause kritik: birden fazla publisher instance’ının aynı eventleri process etmesini önleyerek horizontal scaling sağlıyor.

Polling’in Artıları ve Eksileri#

Artıları:

  • Implement etmesi ve anlaması basit
  • Ek infrastructure gerekmez
  • Herhangi bir database ile çalışır
  • SQL sorguları ile debug kolay

Eksileri:

  • Polling database load ekliyor
  • Latency poll interval’e bağlı (tipik 5-10 saniye)
  • Yüksek volume’ler için CDC’den daha az verimli

Implementasyon Yaklaşımı 2: Change Data Capture (CDC)#

Production sistemlerde scale için CDC, database transaction log’unu doğrudan monitor ederek polling overhead’ini ortadan kaldırıyor.

CDC Nasıl Çalışıyor#

Outbox table’ı poll etmek yerine, Debezium gibi CDC araçları database’in Write-Ahead Log’unu (PostgreSQL) veya Binary Log’unu (MySQL) monitor ediyor. Bir outbox eventi yazıldığında, CDC aracı bunu tespit edip otomatik olarak message broker’a publish ediyor.

CDC Layer

Database

Uygulama

PostgreSQL

Write-Ahead Log

Debezium Connector

Kafka

Consumer Service 1

Consumer Service 2

PostgreSQL + Debezium Setup#

-- 1. Logical replication aktif et (PostgreSQL restart gerektirir)
ALTER SYSTEM SET wal_level = 'logical';
-- Not: wal_level değişikliğinin etkili olması için PostgreSQL restart edilmeli

-- 2. Outbox table için publication oluştur
CREATE PUBLICATION outbox_publication FOR TABLE outbox;

-- 3. Debezium kullanıcısına replication hakları ver
ALTER USER debezium_user WITH REPLICATION;

Debezium Konfigürasyonu#

{
  "name": "outbox-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres.example.com",
    "database.port": "5432",
    "database.user": "debezium_user",
    "database.password": "${DB_PASSWORD}",
    "database.dbname": "orders_db",
    "database.server.name": "orders",
    "table.include.list": "public.outbox",
    "plugin.name": "pgoutput",
    "transforms": "outbox",
    "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
    "transforms.outbox.table.field.event.type": "event_type",
    "transforms.outbox.table.field.event.key": "aggregate_id",
    "transforms.outbox.table.field.payload": "payload"
  }
}

Producer Kodu (Polling ile Aynı)#

CDC’nin güzelliği: uygulama kodun değişmiyor. Hala aynı transaction içinde outbox table’a yazıyorsun. Debezium publishing’i handle ediyor.

// Polling yaklaşımıyla aynı kod - değişiklik gerekmez
async function createOrder(orderData: Order) {
  await db.transaction(async (trx) => {
    await trx('orders').insert(orderData);
    await trx('outbox').insert({
      aggregate_type: 'Order',
      aggregate_id: orderData.id,
      event_type: 'OrderCreated',
      payload: orderData
    });
  });
  // Debezium otomatik olarak yeni outbox satırını tespit edip publish ediyor
}

CDC’nin Artıları ve Eksileri#

Artıları:

  • Gerçek zamana yakın event publishing (< 1 saniye)
  • Minimal database overhead (WAL okuyor, table’ları değil)
  • Throughput, tablo taraması yerine log reader ve broker kapasitesiyle ölçekleniyor
  • Partition başına event sırasını koruyor

Eksileri:

  • Karmaşık infrastructure (Kafka Connect, Debezium)
  • Operasyonel uzmanlık gerektiriyor
  • Database-specific setup (WAL konfigürasyonu)
  • Serverless seçeneklerden daha pahalı

AWS Implementasyonu: DynamoDB + EventBridge Pipes#

AWS, DynamoDB Streams ve EventBridge Pipes kullanarak serverless bir outbox implementasyonu sunuyor. AWS-native mimariler için önerilen yaklaşım budur.

Mimari#

Uygulama

DynamoDB Orders Table

DynamoDB Outbox Table

DynamoDB Stream

EventBridge Pipe

EventBridge Event Bus

Consumer Lambda 1

Consumer Lambda 2

Implementasyon#

// 1. Her iki item'ı tek transaction ile yaz
// Not: DynamoDB transaction limitleri - max 100 item, 4MB toplam boyut
async function createOrder(orderData: Order) {
  await ddb.send(new TransactWriteItemsCommand({
    TransactItems: [
      {
        Put: {
          TableName: 'Orders',
          Item: {
            orderId: { S: orderData.id },
            customerId: { S: orderData.customerId },
            total: { N: orderData.total.toString() },
            status: { S: 'PENDING' }
          }
        }
      },
      {
        Put: {
          TableName: 'Outbox',
          Item: {
            eventId: { S: uuid() },
            aggregateType: { S: 'Order' },
            aggregateId: { S: orderData.id },
            eventType: { S: 'OrderCreated' },
            payload: { S: JSON.stringify(orderData) },
            timestamp: { N: Date.now().toString() }
          }
        }
      }
    ]
  }));
}

Infrastructure as Code (AWS CDK)#

import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as pipes from 'aws-cdk-lib/aws-pipes';
import * as events from 'aws-cdk-lib/aws-events';

// 1. Stream aktif outbox table oluştur
const outboxTable = new dynamodb.Table(this, 'OutboxTable', {
  partitionKey: { name: 'eventId', type: dynamodb.AttributeType.STRING },
  stream: dynamodb.StreamViewType.NEW_IMAGE,  // Kritik: yeni item'ları stream et
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  removalPolicy: cdk.RemovalPolicy.DESTROY
});

// 2. Event bus oluştur
const eventBus = new events.EventBus(this, 'OrderEventBus', {
  eventBusName: 'order-events'
});

// 3. EventBridge Pipe oluştur (Lambda GEREKMİYOR!)
new pipes.CfnPipe(this, 'OutboxPipe', {
  source: outboxTable.tableStreamArn!,
  target: eventBus.eventBusArn,
  roleArn: pipeRole.roleArn,
  sourceParameters: {
    dynamoDbStreamParameters: {
      startingPosition: 'LATEST',
      batchSize: 10,
      maximumRetryAttempts: 3,  // Not: Varsayılan -1 (sonsuz retry)
      deadLetterConfig: {
        arn: dlqQueue.queueArn
      }
    }
  },
  targetParameters: {
    eventBridgeEventBusParameters: {
      detailType: 'OutboxEvent',
      source: 'outbox.publisher'
    }
  }
});

Avantajları#

Publishing için Lambda kodu yok: EventBridge Pipes otomatik olarak DynamoDB Streams’i okuyup EventBridge’e publish ediyor. Bu şunları ortadan kaldırıyor:

  • Cold start latency
  • Publisher için Lambda billing
  • Relay için maintain edilecek kod

Built-in reliability: Pipes retry logic, dead-letter queue ve monitoring’i out-of-the-box sunuyor.

Cost efficiency: Faturanın büyük kısmı event hacmini takip ediyor ve ayakta tutman gereken bir publisher filosu yok.

Maliyet Analizi#

us-east-1 liste fiyatlarıyla, ayda 10 milyon eventlik bir yükün maliyeti:

  • DynamoDB Streams: GetRecords çağrısı başına, 100.000 stream read request unit için $0.02 üzerinden faturalanıyor. Pipes, kayıt beklesin ya da beklemesin her açık shard’ı saniyede dört kez poll ediyor; yani çağrı sayısı batchSize’ı değil, shard sayısını ve geçen süreyi takip ediyor. Sürekli açık tek bir shard ayda yaklaşık 10,4 milyon çağrı, yani kabaca $2. Lambda trigger’larına tanınan muafiyet Pipes’ı kapsamıyor.
  • EventBridge Pipes: milyon istek başına $0.40 = $4.00/ay
  • EventBridge custom event bus: milyon event başına $1.00 = $10.00/ay
  • Toplam: zaten ödediğin DynamoDB write maliyetlerinin üzerine yaklaşık $16/ay

Polling relay’in event başına ücreti olmadığı için faturada her zaman daha ucuz görünüyor. Maliyet yok olmuyor, yer değiştiriyor: poller event olsa da olmasa da çalışıyor, her poll production database’ine giden bir sorgu ve relay birinin sahiplenip izleyeceği, gerekirse gece kaldırılacağı bir kod. İkisi arasında seçim yaparken milyon başına satıra değil, operasyonel yüke bak.

Ordering ve Idempotency Yönetimi#

Ordering Garantileri#

Outbox pattern partition başına sıralamayı koruyor, tüm eventlerde global sıralama değil.

// Aynı aggregate için eventlerin sıralandığından emin ol
await kafka.producer.send({
  topic: 'order-events',
  messages: [{
    key: event.aggregateId,  // ORDER-123 için tüm eventler aynı partition'a gidiyor
    value: JSON.stringify(event.payload)
  }]
});

DynamoDB Streams için aggregate ID’yi partition key olarak kullan:

await dynamodb.put({
  TableName: 'Outbox',
  Item: {
    aggregateId: 'ORDER-123',  // Partition key - sıralamayı garanti ediyor
    eventId: uuid(),  // Sort key
    eventType: 'OrderCreated',
    timestamp: Date.now()
  }
});

Inbox Pattern: Consumer-Side Idempotency#

Outbox pattern at-least-once delivery garanti ediyor, yani eventler birden fazla kez deliver edilebilir. Consumer’lar duplicate’leri handle etmeli.

Inbox Pattern idempotent processing sağlıyor:

async function handleOrderCreatedEvent(event: OrderCreatedEvent) {
  await db.transaction(async (trx) => {
    // 1. Daha önce işlenmiş mi kontrol et
    const existing = await trx('inbox')
      .where('message_id', event.messageId)
      .first();

    if (existing) {
      console.log('Duplicate message, atlanıyor:', event.messageId);
      return;  // Idempotent - güvenle atla
    }

    // 2. Eventi işle (business logic'in)
    await trx('inventory')
      .where('product_id', event.productId)
      .decrement('quantity', event.quantity);

    // 3. İşlenmiş olarak kaydet AYNI TRANSACTION İÇİNDE
    await trx('inbox').insert({
      message_id: event.messageId,
      event_type: event.type,
      processed_at: new Date()
    });

    // Ya üç operasyon da başarılı, ya da hepsi başarısız
  });

  // Sadece başarılı commit'ten sonra message broker'a ACK
  await messageQueue.ack(event.messageId);
}

Inbox table schema:

CREATE TABLE inbox (
  message_id UUID PRIMARY KEY,
  event_type VARCHAR(100),
  processed_at TIMESTAMP DEFAULT NOW(),
  payload JSONB  -- Opsiyonel: debugging için
);

-- Eski işlenmiş mesajları temizle (günlük çalıştır)
DELETE FROM inbox
WHERE processed_at < NOW() - INTERVAL '7 days';

Tam Pattern: Outbox + Inbox#

InboxDBConsumerBrokerRelayOutboxDBProducerInboxDBConsumerBrokerRelayOutboxDBProduceralt[Islenmemis][Zaten islenmis]Data + event yaz (tx)CommitYeni eventleri okuEvent publish etEventi gonderIslenmis mi kontrol etIsle + ID ekle (tx)CommitACKACK (islemeyi atla)

Performans Notları#

Database Performance#

Outbox table büyümesi: Temizlik yapılmazsa outbox table sonsuza kadar büyür ve ciddi performance degradation’a yol açar.

-- Strateji 1: Publish'dan hemen sonra sil
DELETE FROM outbox WHERE id = $1 AND published = true;

-- Strateji 2: Batch temizlik (cron ile günlük çalıştır)
DELETE FROM outbox
WHERE published = true
  AND created_at < NOW() - INTERVAL '7 days';

-- Strateji 3: Table partitioning (PostgreSQL 10+)
CREATE TABLE outbox_2025_12 PARTITION OF outbox
  FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');

-- Eski partition'ları drop et (DELETE'den çok daha hızlı)
DROP TABLE outbox_2025_11;

Index optimizasyonu: Partial index sadece yayınlanmamış eventleri index’liyor, yer tasarrufu sağlıyor:

CREATE INDEX idx_outbox_unpublished
ON outbox(created_at)
WHERE published = false;

Polling Publisher Tuning#

Poll interval trade-off’ları:

  • 1 saniye: Düşük latency, yüksek database load
  • 5 saniye: Dengeli (çoğu durum için önerilen)
  • 10+ saniye: Düşük overhead, yüksek latency

Batch size:

// Çok küçük: çok fazla sorgu, verimsiz
const batchSize = 10;

// Çok büyük: uzun transaction'lar, lock contention
const batchSize = 10000;

// Optimal: verimlilik ve transaction uzunluğu dengesi
const batchSize = 100;  // Önerilen başlangıç noktası

CDC Performance#

Debezium’un yetiştiğinden emin olmak için replication lag’i monitor et:

-- Replication slot lag'ini kontrol et
SELECT slot_name,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as lag
FROM pg_replication_slots
WHERE slot_type = 'logical';

Lag büyürse WAL dosyaları birikir ve disk dolabilir. Production CDC kurulumlarında kritik bir operasyonel concern budur.

Yaygın Tuzaklar ve Çözümler#

Tuzak 1: Sınırsız Table Büyümesi#

Problem: Outbox table sonsuza kadar büyüyor, sorgular yavaşlıyor.

Çözüm: Publisher’ında otomatik temizlik implement et:

async function publishAndCleanup() {
  // Eventleri publish et
  await publishOutboxEvents();

  // Eski published eventleri temizle (her 100 iteration'da bir)
  if (cleanupCounter++ % 100 === 0) {
    await db('outbox')
      .where('published', true)
      .where('created_at', '<', db.raw("NOW() - INTERVAL '7 days'"))
      .delete();
  }
}

Tuzak 2: Message Relay Başarısızlığı Fark Edilmiyor#

Problem: Publisher crash oluyor, eventler publish edilmeden birikiyor.

Çözüm: Outbox yaş metriklerini monitor et:

async function checkOutboxHealth() {
  const result = await db('outbox')
    .where('published', false)
    .min('created_at as oldest')
    .first();

  if (!result.oldest) return;  // Yayınlanmamış event yok

  const ageMs = Date.now() - new Date(result.oldest).getTime();
  const ageMinutes = ageMs / 60000;

  if (ageMinutes > 5) {
    alerting.trigger('OUTBOX_LAG_HIGH', {
      ageMinutes,
      message: 'Outbox eventleri publish edilmiyor'
    });
  }
}

// Health check'i her dakika çalıştır
setInterval(checkOutboxHealth, 60000);

Tuzak 3: CDC Replication Slot Disk Dolduruyor#

Problem: Debezium connector down oluyor, PostgreSQL WAL birikiyor.

Çözüm: Replication slot’ları monitor et ve retention limit’leri belirle:

-- WAL retention limit belirle
ALTER SYSTEM SET wal_keep_size = '10GB';

-- Slot durumunu monitor et
SELECT slot_name, active,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as lag
FROM pg_replication_slots;

Bir slot 5 dakikadan fazla inactive ise alert ver, publisher failure göstergesi.

Diğer Pattern’lerle Karşılaştırma#

Outbox vs. Event Sourcing#

BoyutOutbox PatternEvent Sourcing
AmaçGüvenilir event publishingEventler tek doğruluk kaynağı
Event ÖmrüKısa ömürlü (publish’dan sonra siliniyor)Kalıcı append-only log
State SaklamaCurrent state table’lardaEventlerden türetiliyor
ComplexityDüşükYüksek
Query ModelDirekt database sorgularıProjection/CQRS gerektirir
En İyi KullanımE-ticaret siparişleri, workflow’larBankacılık, audit sistemleri

Temel fark: Event sourcing’de eventler kalıcı kayıt. Outbox’ta eventler bir iletişim mekanizması.

Outbox vs. Saga Pattern#

Outbox pattern saga pattern’i tamamlıyor. Saga’ya katılan her servis içinde outbox kullan:

// Order Service OrderCreated'ı outbox ile publish ediyor
await db.transaction(async (trx) => {
  await trx('orders').insert(order);
  await trx('outbox').insert({ event_type: 'OrderCreated', payload: order });
});

// Saga Orchestrator OrderCreated'ı alıyor, kendi outbox'u ile command'ları publish ediyor
await db.transaction(async (trx) => {
  await trx('saga_state').insert({ saga_id: orderId, step: 'INVENTORY_PENDING' });
  await trx('outbox').insert({ event_type: 'ReserveInventory', payload: { orderId } });
});

Hangi Yaklaşımı Seçmeli#

Başlangıç noktası polling. Aşağıdaki dallar ondan ne zaman ayrılman gerektiğini gösteriyor:

Evet

Hayir

Dusuk < 1K/dk

Yuksek > 1K/dk

Basit

Production

Evet

Hayir

Guvenilir Eventler Gerekli mi?

Event Volume?

Direct Publishing Uygun

Infrastructure Tercihi?

AWS Kullaniyor musun?

Polling Publisher

CDC ile Debezium

DynamoDB + EventBridge Pipes

Polling seç:

  • Event volume < 1000/dakika
  • Hızlı başlamak için
  • Basit mimari tercih ediliyorsa
  • Database CDC desteklemiyorsa

CDC seç:

  • Event volume > 1000/dakika
  • < 1 saniye latency gerekiyorsa
  • Scale’deki production sistemlerde
  • Zaten Kafka kullanıyorsan

DynamoDB + EventBridge seç:

  • AWS üzerinde geliştiriyorsan
  • Serverless mimari istiyorsan
  • Minimal operasyonel overhead isteniyorsa
  • Orta volume’ler için cost-effective

Production Readiness Checklist#

Outbox pattern’i production’a deploy etmeden önce:

  • Cleanup stratejisi: Published eventlerin otomatik silinmesi
  • Monitoring: Outbox age, backlog size, publisher health
  • Alerting: Lag threshold’u aşıyor, publisher failure’ları
  • Idempotency: Inbox pattern veya idempotency key’leri implement edildi
  • Ordering: Event sıralaması için partition key stratejisi
  • Dead Letter Queue: Başarısız eventler inceleme için yönlendiriliyor
  • Schema versioning: Event payload versioning stratejisi
  • Load testing: Beklenen throughput’ta doğrulandı
  • Runbook: Recovery prosedürleri dokümante edildi
  • Backup stratejisi: Outbox ve inbox table’ları için

Varsayılan Nerede Geçerli#

Polling, outbox sıcak bir tabloya dönüşene ya da poll interval’i kullanıcıya hissettirmeye başlayana kadar geçerli. Sonrası iki yönden birine sapıyor: hacim Kafka Connect’i işletmeyi ve replication slot’ları izlemeyi hak ediyorsa CDC, veri zaten DynamoDB’deyse ve hiç relay kodu yazmak istemiyorsan DynamoDB Streams’ten EventBridge Pipe’a giden yol.

Seçtiğin relay teslimat garantisini değiştirmiyor. Her durumda at-least-once kalıyor; geriye hiçbir implementasyonun atlayamayacağı iki parça kalıyor: consumer tarafında idempotency ve outbox temizliği.

Kaynaklar#

İlgili yazılar