.NET MİCROSERVİCES MESAJLAŞMA: RABBİTMQ/KAFKA MANTIĞI, IDEMPOTENCY VE RETRY
Mikroservis mimarisinde en sık yaşanan sürprizlerden biri, sistem büyüdükçe “başarılı görünen” çağrıların gerçekte başarısız zincirler üretmesidir. Bir servis yanıt veremediğinde, diğerleri onu bekler; kuyruklar dolmaya başlar; yeniden denemeler aynı hatayı tekrarlar ve sonunda operasyon ekibi, “neden her şey bir anda yavaşladı?” sorusuyla karşı karşıya kalır. Bu yüzden .NET microservices mesajlaşma yaklaşımı, yalnızca entegrasyon tercihi değil, dayanıklılık stratejisidir.
Mesajlaşma dediğimiz şey, HTTP yerine her zaman kuyruk kullanmak değildir. Asıl amaç; servisler arası bağımlılığı azaltmak, yükü dengelemek, geçici hataları tolere etmek ve iş akışlarını olaylar üzerinden kurgulamaktır. RabbitMQ ve Kafka gibi message broker çözümleri, farklı problem tiplerinde güçlüdür; doğru seçim, doğru idempotency ve doğru retry yaklaşımı ile birleştiğinde mimariyi sürdürülebilir kılar.
Bu yazıda RabbitMQ/Kafka mantığını netleştirip, pratik seçim kriterleri çıkaracağız. Ardından idempotency ile “aynı mesaj iki kez geldiğinde ne olacak?” sorusunu çözeceğiz. Son olarak retry stratejilerini; backoff, DLQ ve zehirli mesaj yönetimi gibi operasyonel detaylarla birlikte ele alacağız. Daha kapsamlı uygulamalı bir akış için .NET Microservices Eğitimi sayfasına da göz atabilirsiniz.

Primary odak: .NET microservices mesajlaşma yaklaşımını doğru konumlamak
Primary keyword olarak .NET microservices mesajlaşma ifadesini ele alırsak, burada hedef; servislerin birbirini doğrudan çağırmak yerine olaylar ve komutlar üzerinden koordine olmasıdır. Bu yaklaşım, iki temel fayda sağlar: yük dalgalanmalarını emme ve servis bağımlılıklarını gevşetme. Bu sayede bir servis geçici olarak yavaşlasa bile sistem bütünü ayakta kalır.
Yine de mesajlaşma her sorunu otomatik çözmez. Senkron çağrıların sağladığı “anında sonuç” hissi kaybolur ve bunun yerine eventual consistency (sonunda tutarlılık) gibi kavramları benimsemek gerekir. Bu noktada mesaj tasarımı, tüketici davranışı ve gözlemlenebilirlik (observability) kritik hale gelir.
Senkron mu asenkron mu: yanlış ikilikten kurtulmak
Bir sipariş oluşturma akışını düşünün: “Siparişi al, ödemeyi çek, stok düş, kargoyu başlat.” Bu zincirin tamamını tek bir HTTP çağrısıyla yapmaya çalışmak, en zayıf halkayı tüm akışın kilidi haline getirir. Asenkron mesajlaşma; ödeme veya stok gibi adımları ayrı olaylarla ilerleterek, geçici sorunların kuyruğa alınmasını sağlar. Ancak bazı adımlarda kullanıcı deneyimi için hâlâ senkron yanıt gerekecektir. Bu nedenle en iyi sonuç, hibrit yaklaşımla elde edilir.
Mesaj tipleri: komut, olay, entegrasyon olayı
Komut (command), belirli bir işi yapmayı hedefler: “ChargePayment”. Olay (event), olmuş bitmiş bir durumu yayınlar: “PaymentCaptured”. Entegrasyon olayı ise farklı bounded context’ler arasında paylaşılan, dışa dönük sözleşmedir. İsimlendirme ve semantik tutarlılık sağlandığında, mesaj akışları hem daha okunur hem de daha yönetilebilir olur.

RabbitMQ vs Kafka: broker mantığı ve seçim kriterleri
RabbitMQ mantığı, klasik message queue ve routing kavramlarına dayanır. Exchange üzerinden mesajları farklı kuyruklara yönlendirir; work queue modeliyle iş dağıtımı ve ack mekanizmasıyla güvenilir teslimat sağlar. Kafka mantığı ise dağıtık log yaklaşımıdır: konulara (topic) yazılan kayıtlar sıralı olarak tutulur; consumer group’lar bu log’u okuyarak ölçeklenir. Bu iki yaklaşım, farklı iş ihtiyaçlarında öne çıkar.
RabbitMQ’nun güçlü olduğu senaryolar
- İş dağıtımı ve görev kuyrukları: kısa ömürlü işler, hızlı ack akışı
- Esnek routing: topic/fanout/direct exchange ile kural bazlı yönlendirme
- Gecikmeli teslimat, öncelik kuyrukları, DLQ gibi operasyonel araçlar
RabbitMQ; “bu işi bir kere yap, başarıyla işlendiğini bil” tipinde workflow’larda etkilidir. Dead-letter queue (DLQ) ile hatalı mesajları ayrı bir hatta toplayıp analiz etmek de pratik bir yaklaşım sunar.
Kafka’nın güçlü olduğu senaryolar
- Yüksek hacimli event stream: çok sayıda olay, yüksek throughput
- Event sourcing benzeri ihtiyaçlar: kayıtların log olarak saklanması
- Consumer group ile ölçeklenebilir okuma, partition mantığıyla paralellik
Kafka; olayları yalnızca “iletilen mesaj” değil, aynı zamanda “geçmişin kaydı” olarak ele alır. Bu sayede yeni bir servis eklendiğinde, geçmiş veriyi okuyarak kendi state’ini inşa etmesi mümkündür. Bu yaklaşım, yeniden oynatma (replay) gibi kavramları gündeme getirir.
Mesaj sözleşmesi: şema, versiyonlama ve uyumluluk
Mesajlaşmada en hızlı borç biriktiren alan, mesaj şemasıdır. Alan adları, enum değerleri, tip dönüşümleri ve nullable kararları; üretici ve tüketici tarafında zamanla uyumsuzluk üretir. Bu yüzden tasarımın başında “mesaj sözleşmesi” prensiplerini belirlemek gerekir.
Versiyonlama stratejisi: geriye uyumluluk tasarlamak
Temel kural: mevcut tüketicileri kırmadan değişiklik yapmak. Pratikte bu; alan eklerken geriye uyumluluk, alan kaldırırken uzun deprecation süreci, enum genişletirken default davranış gibi kararlar anlamına gelir. Eğer JSON kullanıyorsanız, ek alanlar çoğunlukla sorun çıkarmaz; ancak alan isimlerinin değiştirilmesi risklidir. Avro/Protobuf gibi şema odaklı yaklaşımlar ise uyumluluğu daha sistematik yönetmenizi sağlar.
İçerik zenginliği: olayın niyeti mi, state kopyası mı?
Bir “OrderCreated” olayı; yalnızca OrderId içerebilir ya da sipariş kalemleri ve toplam tutarı da taşıyabilir. Fazla veri taşımak, üreticiye bağımlılığı artırabilir; az veri taşımak ise tüketicide ekstra sorgular üretir. Bu dengeyi; veri erişim maliyeti, gecikme toleransı ve veri tutarlılığı ihtiyacı belirler. Çoğu senaryoda, entegrasyon olaylarında gerekli minimum bilgiyi taşımak ve kalanını tüketici tarafında kendi modeline göre inşa etmek daha sağlıklıdır.

Idempotency: aynı mesaj iki kez gelirse ne olur?
Dağıtık sistemlerde “en az bir kez teslim” (at-least-once) yaygın bir gerçektir. Ağ kopması, tüketicide timeout, broker tarafındaki yeniden gönderim veya üreticideki tekrar yayın; aynı mesajın iki kez işlenmesine yol açabilir. İşte idempotency, bu tekrarların yan etkisiz olmasını sağlamaktır.
Idempotency anahtarı: mesaj kimliği ve iş etkisi
En pratik yaklaşım, her mesaj için bir MessageId üretmek ve tüketicide bu MessageId’nin daha önce işlendiğini kalıcı bir store’da kaydetmektir. Böylece ikinci kez gelen mesaj “zaten işlendi” olarak işaretlenir. Burada kritik nokta, bu kontrolün iş etkisiyle aynı transaction sınırında yapılmasıdır; aksi halde yarış koşulları (race condition) ortaya çıkar.
.NET örneği: EF Core ile tüketici tarafı idempotency tablosu
Aşağıdaki örnekte, tüketici bir “PaymentCaptured” mesajı aldığında, önce ProcessedMessages tablosuna bakar. Eğer MessageId işlenmişse, iş yapmadan çıkar. Değilse; iş etkisini uygular ve MessageId’yi kaydeder.
public sealed class ProcessedMessage
{
public Guid Id { get; set; } // MessageId
public DateTimeOffset ProcessedAt { get; set; }
public string Consumer { get; set; } = default!;
}
public async Task HandleAsync(PaymentCaptured message, AppDbContext db, CancellationToken ct)
{
var already = await db.ProcessedMessages
.AnyAsync(x => x.Id == message.MessageId && x.Consumer == "Billing.PaymentCapturedConsumer", ct);
if (already)
return;
// İş etkisi: örn. fatura oluşturma veya ödeme kaydı
db.BillingEntries.Add(new BillingEntry
{
OrderId = message.OrderId,
Amount = message.Amount,
CapturedAt = message.CapturedAt
});
db.ProcessedMessages.Add(new ProcessedMessage
{
Id = message.MessageId,
ProcessedAt = DateTimeOffset.UtcNow,
Consumer = "Billing.PaymentCapturedConsumer"
});
await db.SaveChangesAsync(ct);
}Bu yaklaşım, basit ve etkilidir. Ancak veritabanı yükünü artırabilir. Yüksek hacimli stream’lerde TTL, partition’lı storage veya daha kompakt bir dedup store tercih edilebilir. Yine de kritik iş akışlarında “yanlışlıkla iki kez faturalama” gibi senaryoların maliyeti, bu tablonun maliyetinden daha yüksektir.
Retry stratejileri: güvenli yeniden deneme ve zehirli mesaj yönetimi
Retry, geçici hatalarda sistemi kurtarır; yanlış kullanıldığında ise sistemi çökertir. En sık hata; her hatayı aynı şekilde ele almak ve hızlı retry ile bağımlı servisi daha da boğmaktır. Doğru retry; hata türünü sınıflandırır, backoff uygular, sınırlandırır ve gerektiğinde mesajı karantinaya alır.
Geçici hata vs kalıcı hata ayrımı
Geçici hatalar: timeouts, kısa süreli ağ kopmaları, bağımlı serviste kısa süreli yoğunluk. Kalıcı hatalar: doğrulama hatası, domain kuralı ihlali, eksik veri. Kalıcı bir hatayı 100 kez denemek, yalnızca kuyruğu şişirir. Bu nedenle hata sınıflandırması ve ölçümleme şarttır.
Exponential backoff ve jitter: stampede etkisini azaltmak
Backoff, her denemede bekleme süresini artırır; jitter ise bekleme sürelerine küçük rastgelelik ekleyerek aynı anda yüklenmeyi engeller. Bu ikili, özellikle bağımlı servis geri döndüğünde “herkes aynı anda saldırdı” etkisini azaltır. Ayrıca maksimum deneme sayısı ve maksimum toplam süre gibi sınırlar belirlenmelidir.
.NET örneği: Polly ile retry + circuit breaker yaklaşımı
Mesaj tüketicisi içinde dış servise çağrı yapıyorsanız, sadece retry değil, devre kesici (circuit breaker) de düşünmelisiniz. Aşağıdaki örnek; geçici HTTP hatalarında retry ve sık hatada circuit breaker uygular.
using Polly;
using Polly.Extensions.Http;
using System.Net;
var retry = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (outcome, timespan, attempt, context) => { /* log/metric */ });
var breaker = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 10,
durationOfBreak: TimeSpan.FromSeconds(30));
var policy = Policy.WrapAsync(retry, breaker);
// kullanım
var response = await policy.ExecuteAsync(() => httpClient.GetAsync("/inventory/check", ct));Bu yaklaşımı broker retry mekanizmalarıyla karıştırmamak gerekir. Broker seviyesinde retry; mesajı yeniden teslim eder. Uygulama seviyesinde retry ise dış bağımlılığa çağrıyı yeniden dener. Çoğu senaryoda ikisini birlikte kullanırken, toplam yükü kontrol altında tutacak bir tasarım gerekir.
DLQ, poison message ve yeniden işleme süreci
Her sistemde zehirli mesajlar olur: beklenmeyen veri, uyumsuz şema, yanlış encoding, domain kuralı ihlali gibi durumlar. Bu mesajlar, sonsuz retry döngüsüne girerse, tüketicinin ilerlemesini engeller. Bu yüzden DLQ veya “parked queue” yaklaşımı, operasyonel bir zorunluluktur.
DLQ akışı: otomatik yönlendirme ve insan kontrollü müdahale
Pratik bir süreç şu adımları izler: sınırlı retry sonrası mesaj DLQ’ya düşer, alarm tetiklenir, mesaj içeriği ve hata bağlamı incelenir, gerekiyorsa düzeltme uygulanır ve mesaj kontrollü biçimde yeniden işlenir. Burada en kritik nokta, mesajın “neden” DLQ’ya düştüğünün izlenebilir olmasıdır; yalnızca exception metni yeterli değildir.
Yeniden işleme: aynı hatayı tekrar üretmeden geri döndürmek
DLQ’dan geri alma işlemi, doğrudan “tekrar aynı kuyruğa bas” değildir. Önce kök neden çözülmeli; ardından idempotency sayesinde geri dönüş güvenli olmalıdır. Aksi halde, aynı mesaj yeniden DLQ’ya düşer ve operasyon döngüsü kilitlenir.

Outbox/Inbox pattern: veri tutarlılığı ve güvenilir yayın
Mikroservislerde en zor problemlerden biri şudur: veritabanına yazdınız ama olayı yayınlayamadınız; ya da olayı yayınladınız ama veritabanına yazılamadı. Bu ikilemi çözmek için outbox pattern kullanılır. İşlem sırasında, olay veritabanında bir outbox tablosuna yazılır; arka planda çalışan bir publisher bu kaydı broker’a güvenli biçimde gönderir.
Outbox ile “atomiklik” illüzyonunu yönetmek
Outbox, tek transaction içinde hem domain değişikliğini hem de yayınlanacak olayı aynı store’a yazar. Böylece işlem başarısız olursa ikisi de olmaz. Publisher tarafı başarısız olursa, outbox kaydı durur ve tekrar denenir. Bu yaklaşım, özellikle ödeme ve sipariş gibi kritik akışlarda “kayıp olay” riskini ciddi ölçüde azaltır.
Inbox pattern: tüketicide tekrar teslimatın etkisini azaltmak
Inbox, tüketicide gelen mesajları önce kalıcı store’a kaydedip sonra işlemesini sağlar; idempotency ile birlikte çalışır. Bazı senaryolarda bu yaklaşım, mesajın işlenmesi sırasında yaşanan çökme durumunda “yarım iş” riskini azaltır. Hangi pattern’lerin gerekli olduğu, iş kritikliği ve sistemin hata toleransına göre belirlenir.
Kafka tarafında pratik noktalar: consumer group, offset ve yeniden oynatma
Kafka kullanırken “mesaj geldi ve işlendi” kavramı, offset yönetimiyle bağlantılıdır. Consumer bir kaydı okur, işini yapar ve offset’i commit eder. Eğer commit öncesi çökerse, aynı kayıt tekrar okunur; bu nedenle idempotency yine önemlidir. Ayrıca partition sayısı, paralellik ve sıralama garantisi için temel belirleyicidir.
Partition ve sıralama: anahtar seçiminin etkisi
Sipariş bazlı sıralama istiyorsanız, partition key olarak OrderId kullanmak yaygındır. Böylece aynı siparişe ait olaylar aynı partition’a düşer ve tüketicide sıralı işlenir. Yanlış anahtar seçimi; hem dengesiz yük dağılımı (hot partition) hem de istenmeyen sıralama sorunları doğurabilir.
Gözlemlenebilirlik: log, metric ve trace olmadan mesajlaşma eksik kalır
Mesajlaşma altyapısı kurulduğunda, “işliyor gibi” görünmesi kolaydır; fakat gerçek değer, hata anında hızla teşhis koyabilmektir. Bu yüzden correlation id, trace id, tüketici gecikmesi (consumer lag), retry sayıları ve DLQ hacmi gibi metrikler izlenmelidir. Observability eksikse, küçük bir sorun büyük bir kesintiye dönüşebilir.
Ölçmeniz gereken temel metrikler
- Queue depth / topic lag: birikim var mı?
- Processing time: tüketici başına ortalama süre ve p95
- Retry ve DLQ oranı: kalite ve uyumluluk sinyalleri
- Broker bağlantı hataları ve throughput değişimi
Bu metrikleri, iş akışlarındaki KPI’larla birlikte okumak gerekir. Örneğin DLQ artışı, her zaman kötü değildir; bazen yeni bir versiyonun şema uyumsuzluğunu işaret eden hızlı bir uyarıdır.
Pratik kontrol listesi: sağlam bir mesajlaşma tasarımı için
Yazıyı somutlaştırmak için, projeye başlarken şu kontrol listesini kullanabilirsiniz. Amaç, RabbitMQ/Kafka seçiminden bağımsız olarak idempotency ve retry risklerini erken aşamada yönetmektir.
- Mesaj sözleşmesi: alan adları, versiyonlama, geriye uyumluluk kuralı
- Idempotency: MessageId, dedup store, transaction sınırları
- Retry: geçici hata sınıflandırması, exponential backoff, maksimum sınırlar
- DLQ süreci: alarm, analiz, düzeltme, kontrollü yeniden işleme
- Outbox/Inbox: kritik akışlarda güvenilir yayın ve güvenli tüketim
- Observability: correlation id, lag, retry/DLQ metrikleri, trace zinciri
Sonuç olarak .NET mikroservislerde mesajlaşma, yalnızca broker kurmak değil; operasyonel dayanıklılığı tasarlamaktır. RabbitMQ ve Kafka, farklı güçlü yönler sunar; ancak ikisinde de idempotency ve doğru retry stratejileri olmadan, “rastgele iki kez işleme” ve “sonsuz yeniden deneme” gibi sorunlarla karşılaşmanız kaçınılmazdır. Doğru sözleşme, doğru akış ve doğru gözlemleme ile mesajlaşma, sisteminizi daha esnek ve ölçeklenebilir bir zemine taşır.


