← Blog

e-Fatura ve Entegrasyonlarda Sağlam Mimari: Outbox, Tekrar Deneme ve Idempotency

Entegrasyonlar dış sistem kapalıyken, yanıt geç geldiğinde ya da aynı istek iki kez gittiğinde sınanır. Outbox deseni, akıllı tekrar deneme ve idempotency ile veri kaybetmeyen, çift kayıt üretmeyen entegrasyonlar kurmak.

Bir entegrasyon, her şey yolundayken değil, bir şeyler ters gittiğinde sınanır. e-Fatura servisine istek gönderdiniz ve 30 saniye yanıt gelmedi: fatura gitti mi, gitmedi mi? Tekrar gönderirseniz çift fatura oluşur mu? Bu sırada uygulama yeniden başlarsa kuyruktaki işler ne olur?

Bu yazıda e-Fatura, banka, pazaryeri ve kargo entegrasyonlarında kullandığımız üç temel deseni anlatıyoruz: outbox, akıllı tekrar deneme ve idempotency.

Sorun: iki sistemi aynı anda güncellemek

En sık yapılan hata, kendi veritabanınızı güncelleyip aynı istek içinde dış servisi çağırmaktır:

// Riskli: veritabanı kaydı başarılı, dış çağrı başarısız olursa ne olacak?
db.Faturalar.Add(fatura);
await db.SaveChangesAsync(ct);
await eFaturaClient.GonderAsync(fatura, ct);   // zaman aşımı, ağ hatası, servis bakımda…

Kayıt veritabanında var, ama dış sisteme gidip gitmediği belirsiz. Sıralamayı ters çevirmek de sorunu çözmez; bu kez fatura gönderilmiş ama sizin tarafınızda kaydı olmayabilir.

1. Outbox deseni: önce kaydet, sonra güvenle gönder

Outbox deseninde dış sisteme gönderilecek mesaj, iş verisiyle aynı veritabanı işleminde bir tabloya yazılır. Gönderimi ise arka planda çalışan ayrı bir işçi yapar.

CREATE TABLE dbo.Outbox (
    Id           bigint IDENTITY PRIMARY KEY,
    Tur          nvarchar(100)  NOT NULL,   -- ör. EFaturaGonder
    Anahtar      nvarchar(100)  NOT NULL,   -- idempotency anahtarı
    Veri         nvarchar(max)  NOT NULL,   -- JSON
    Durum        tinyint        NOT NULL DEFAULT 0,  -- 0 bekliyor, 1 tamam, 2 hata
    Deneme       int            NOT NULL DEFAULT 0,
    SonrakiDeneme datetime2     NOT NULL DEFAULT SYSUTCDATETIME(),
    SonHata      nvarchar(2000) NULL,
    CONSTRAINT UQ_Outbox_Tur_Anahtar UNIQUE (Tur, Anahtar)
);

Fatura kaydı ve outbox kaydı birlikte ya başarılı olur ya da birlikte geri alınır. "Kaydedildi ama gönderilmedi" durumu artık kaybolmaz; sadece henüz gönderilmemiştir.

await using var tx = await db.Database.BeginTransactionAsync(ct);

db.Faturalar.Add(fatura);
db.Outbox.Add(OutboxMesaji.Olustur("EFaturaGonder", anahtar: fatura.Ettn.ToString(), fatura.ToDto()));

await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);

2. Arka plan işçisi ve akıllı tekrar deneme

Outbox tablosunu düzenli aralıklarla okuyan bir BackgroundService, bekleyen mesajları gönderir. Başarısız olanlar hemen değil, giderek artan aralıklarla (exponential backoff) yeniden denenir. Böylece bakımdaki bir servise saniyede yüzlerce istek gönderip durumu kötüleştirmezsiniz.

internal sealed class OutboxIsleyici(IServiceScopeFactory scopes, ILogger<OutboxIsleyici> log) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using var scope = scopes.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<ErpDbContext>();
            var gonderici = scope.ServiceProvider.GetRequiredService<IEntegrasyonGonderici>();

            var bekleyenler = await db.Outbox
                .Where(m => m.Durum == OutboxDurum.Bekliyor && m.SonrakiDeneme <= DateTime.UtcNow)
                .OrderBy(m => m.Id).Take(20).ToListAsync(ct);

            foreach (var m in bekleyenler)
            {
                try
                {
                    await gonderici.GonderAsync(m, ct);
                    m.Durum = OutboxDurum.Tamamlandi;
                }
                catch (Exception ex) when (ex is not OperationCanceledException)
                {
                    m.Deneme++;
                    m.SonHata = ex.Message;
                    m.SonrakiDeneme = DateTime.UtcNow.AddSeconds(Math.Min(3600, Math.Pow(2, m.Deneme) * 15));
                    if (m.Deneme >= 10) m.Durum = OutboxDurum.Hata;   // insan müdahalesi gerekir
                    log.LogWarning(ex, "Outbox {Id} gönderilemedi (deneme {Deneme})", m.Id, m.Deneme);
                }
            }
            await db.SaveChangesAsync(ct);
            await Task.Delay(TimeSpan.FromSeconds(5), ct);
        }
    }
}

Tek bir HTTP çağrısı içindeki geçici hatalar (anlık ağ kopması, 503 yanıtı) için ise HTTP istemcisine standart bir dayanıklılık katmanı eklemek yeterlidir:

builder.Services.AddHttpClient<EFaturaClient>(c => c.Timeout = TimeSpan.FromSeconds(60))
    .AddStandardResilienceHandler();   // Microsoft.Extensions.Http.Resilience

3. Idempotency: aynı isteği iki kez göndermek güvenli olsun

Tekrar deneme mekanizması kaçınılmaz olarak şu riski doğurur: ilk istek aslında başarılı olmuş, sadece yanıtı size ulaşmamıştır. İkinci gönderimde çift kayıt oluşmamalıdır.

Bunun yolu her işleme benzersiz ve değişmeyen bir anahtar vermektir:

  • e-Faturada faturanın ETTN değeri doğal bir idempotency anahtarıdır.
  • Banka ödemelerinde ödeme talimatı numarası, pazaryerinde sipariş numarası kullanılabilir.
  • Karşı sistem destekliyorsa bu anahtar istekle birlikte gönderilir; desteklemiyorsa, tekrar göndermeden önce "bu anahtarla bir kayıt var mı?" diye sorgulanır.

Kendi API'lerinizi tasarlarken de aynı ilkeyi uygulayın: istemciden gelen bir idempotency anahtarını benzersiz index'li bir tabloda saklamak, aynı isteğin ikinci kez işlenmesini veritabanı seviyesinde engeller.

4. Görünürlük: entegrasyon paneli

Desenler ne kadar sağlam olursa olsun, bir gün bir mesaj "hata" durumunda kalacaktır. Bu durumda önemli olan, sorunun müşteriden önce sizin tarafınızdan fark edilmesidir. Her entegrasyon projesinde basit bir izleme ekranı kuruyoruz:

GöstergeNeden önemli?
Bekleyen mesaj sayısıArtıyorsa karşı sistem yavaş veya kapalı
Hata durumundaki mesajlarİnsan müdahalesi bekleyen işler
Ortalama gönderim süresiPerformans düşüşünü erkenden gösterir
Son başarılı gönderim zamanı"Sessiz" kesintileri yakalar

Hata alan bir mesajın ayrıntısı, son hata metni ve "yeniden dene" butonu bu ekranda olmalıdır. Böylece destek ekibi veritabanına girmeden sorunu çözebilir.

Özet

  1. İş verisi ve gönderilecek mesajı aynı işlemde kaydedin (outbox).
  2. Gönderimi arka planda, artan aralıklarla tekrar deneyerek yapın.
  3. Geçici hataları tekrar deneyin, iş hatalarını hemen raporlayın.
  4. Her işleme değişmeyen bir anahtar verin; çift kaydı imkânsız hale getirin.
  5. Entegrasyonları bir panelden izleyin.

Bu desenler e-Fatura'dan pazaryerine, bankadan kargo firmasına kadar tüm entegrasyonlarda aynı şekilde işe yarar. Mevcut entegrasyonlarınızda kayıp veya çift kayıt sorunları yaşıyorsanız, yapıyı birlikte gözden geçirebiliriz.

Benzer bir projeniz mi var?

İhtiyacınızı anlatın; size özel bir yol haritası ve tahmini bir plan çıkaralım.

Bize Ulaşın →