Mikroservis mimarisine geçişin o tatlı sarhoşluğunu hepimiz yaşamışızdır: Bağımsız deploylar, teknoloji özgürlüğü, ölçeklenebilirlik hayalleri... Monolitin zincirlerinden kurtulmak harika bir histi. Ta ki bir Cuma akşamı, veritabanına kaydedilmiş bir siparişin stok servisinin ruhu bile duymadığını fark edene kadar. İşte o an, dağıtık sistemlerin acımasız gerçeğiyle tanışırız: veri tutarlılığı.
Monolitik dünyada @Transactional anotasyonunun sihirli koruması altındaydık. Ya hep ya hiç. Peki ya şimdi? Bir servis kendi veritabanına yazarken diğeri bu işlemden nasıl haberdar olacak? Veritabanı COMMIT dedikten bir milisaniye sonra çökme yaşanırsa ne olacak?
Bu gibi kabus senaryoları, sadece "Dual Write" (İkili Yazma) tutarsızlıklarından değil, aynı zamanda mesaj sistemlerinin anlık olarak ulaşılamaz olmasından da kaynaklanır. Peki, bir yandan kendi veritabanımıza atomik olarak yazarken, diğer yandan bu değişikliği diğer servislere ne olursa olsun ulaşacağını garanti edecek şekilde nasıl duyurabiliriz? Bu yazıda, tam olarak bu güvenilir ve asenkron iletişimi sağlayan, sistemlerimizi kaosa karşı kurşun geçirmez hale getiren Outbox ve Inbox desenlerini, yani iki hayat kurtaran "emniyet kemeri"ni inceleyeceğiz.
Temel Problem: "Ya Biri Olur, Diğeri Olmazsa?"
Senaryomuz basit: Bir kullanıcı "Siparişi Tamamla" butonuna bastı. Bizim backend tarafında yapmamız gereken iki kritik iş var:
- Siparişi kendi yerel veritabanımıza kaydetmek (
ordertablosu). - Diğer servislerin haberdar olması için RabbitMQ'ya bir
OrderCreatedevent'i atmak.
Tipik bir Spring Boot servisinde kodu şöyle yazdığımızı varsayalım:
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final RabbitTemplate rabbitTemplate;
@Transactional
public void createOrder(OrderRequest request) {
// 1. Veritabanına kaydet
Order order = orderRepository.save(new Order(request));
// 2. Mesaj kuyruğuna gönder (TEHLİKELİ BÖLGE)
rabbitTemplate.convertAndSend("orders.exchange", "order.created", new OrderCreatedEvent(order.getId()));
}
}
Bu kod %99 ihtimalle sorunsuz çalışır. Peki ya o %1'lik felaket senaryosu? Veritabanına COMMIT gittikten sonra, ama RabbitMQ'ya bağlantı kurulmadan önce sunucu çökerse ne olur? Sonuç: Veritabanınızda bir sipariş var ama ne stok düştü ne de kargo servisinin haberi var. Sistem tutarsız bir duruma (inconsistent state) düştü.
Dual-Write Neden Bir Anti-Pattern'dir?
Yukarıdaki senaryo, "Dual Write" olarak bilinen ve dağıtık sistemlerde kaçınılması gereken bir anti-pattern'dir. Problem, iki bağımsız sistemi (örneğin, bir PostgreSQL veritabanı ve bir RabbitMQ broker'ı) tek bir atomik işlem gibi yönetmeye çalışmaktan kaynaklanır. Bu sistemlerin kendilerine ait transaction mekanizmaları vardır ve bunlar arasında 2-Phase-Commit (2PC) gibi karmaşık ve pahalı protokoller olmadan bir atomiklik garantisi verilemez.
Dual-Write'ın getirdiği riskler şunlardır:
- Veri Kaybı: Mesaj broker'ına yapılan çağrı başarısız olursa, olay (event) sonsuza dek kaybolur.
- Veri Tutarsızlığı: Veritabanı işlemi başarılı olur ama mesaj gönderimi başarısız olursa, sistemin bir kısmı güncellenirken diğer kısımları eski veride kalır.
- Geçici Hatalar: Mesaj broker'ı anlık olarak ulaşılamaz olduğunda bile tüm işlem başarısız olur ve kullanıcıya hata döner. Bu, sistemin dayanıklılığını (resilience) düşürür.
İşte bu noktada Outbox ve Inbox pattern'ları, bu kaosu yönetmek için birer "emniyet kemeri" görevi görür.
1. Outbox Pattern: Gönderici Tarafının Sigortası
Outbox pattern'in arkasındaki fikir dâhice olduğu kadar basittir: Eğer dışarıya bir mesaj göndereceksen, bunu da veritabanı işleminin bir parçası haline getir.
Mesajı doğrudan RabbitMQ'ya atmak yerine, veritabanımızdaki özel bir outbox tablosuna "Bu mesaj gönderilecek" diye bir kayıt atarız. Bu işlem, asıl iş verisiyle aynı transaction içinde yapılır.
@Transactional
public void createOrder(OrderRequest request) {
// 1. Asıl iş verisini kaydet
Order order = orderRepository.save(new Order(request));
// 2. Gönderilecek mesajı AYNI DB TRANSACTION içinde Outbox tablosuna yaz
OutboxEntity outboxEvent = OutboxEntity.builder()
.aggregateType("Order")
.aggregateId(order.getId().toString())
.type("OrderCreated")
.payload(objectMapper.writeValueAsString(new OrderCreatedEvent(order.getId())))
.build();
outboxRepository.save(outboxEvent);
}
// Metot bittiğinde Spring her iki kaydı da (Order + Outbox) atomik olarak commit eder.
Veritabanı Transaction'ının Gücü
Bu yaklaşımın kilit noktası, modern ilişkisel veritabanlarının sunduğu ACID (Atomicity, Consistency, Isolation, Durability) garantilerinden sonuna kadar faydalanmaktır. @Transactional anotasyonu sayesinde, Order nesnesinin veritabanına yazılması ile OutboxEntity nesnesinin yazılması aynı "iş biriminin" parçası haline gelir.
Bu bize şunu garanti eder: Eğer sipariş veritabanına kaydedildiyse, gönderilecek mesaj da kesinlikle Outbox tablosuna kaydedilmiştir. Veritabanı COMMIT komutunu çalıştırmadan hemen önce çökse bile, transaction başarılı olamayacağı için her iki kayıt da geri alınır (rollback). Bu sayede "hayalet" bir siparişin olayını gönderme veya olayı gönderilmemiş bir sipariş oluşturma riski ortadan kalkar.
Outbox Tablosunun Anatomisi
Etkili bir outbox tablosu genellikle şu alanları içerir:
id: Her olay kaydı için benzersiz bir anahtar.aggregate_type: Olayın hangi iş nesnesiyle ilgili olduğu (örn: "Sipariş", "Kullanıcı").aggregate_id: İlgili nesnenin ID'si (örn: siparişin ID'si).event_type: Olayın türünü belirten bir string (örn: "OrderCreated", "PasswordUpdated").payload: Asıl mesajın içeriği. Genellikle JSON formatında saklanır.status: Mesajın durumunu takip etmek için kullanılır (örn:PENDING,SENT). Relay bu alanı günceller.
Bu yapı, sadece mesaj göndermeyi garanti altına almakla kalmaz, aynı zamanda sistemde ne olup bittiğine dair harika bir denetim kaydı (audit log) da sunar.
Peki Mesaj Kuyruğa Nasıl Gidecek? (The Relay)
outbox tablosu bir ara duraktır. Mesajları buradan alıp asıl hedefi olan RabbitMQ'ya iletecek bir mekanizmaya (Relay) ihtiyaç duyarız. İki yaygın yöntem vardır:
- Polling (Basit): Spring'de
@Scheduledile çalışan bir job,outboxtablosunu periyodik olarak sorgular, "gönderilmemiş" mesajları bulur, RabbitMQ'ya atar ve mesajı "gönderildi" olarak işaretler veya siler. Basit ve etkilidir ancak küçük bir gecikme yaratır ve veritabanına sürekli sorgu atar. - CDC (Change Data Capture - İleri Seviye): Debezium gibi bir araç, veritabanının transaction loglarını (örn. PostgreSQL WAL) dinler.
outboxtablosuna bir kayıt düştüğü anda bunu yakalayıp neredeyse anlık olarak Kafka veya RabbitMQ'ya basar. Son derece verimli ve hızlıdır, uygulama koduna dokunmanızı bile gerektirmez.
Bu pattern'in önemli bir sonucu, bize "At-least-once delivery" (en az bir kez teslimat) garantisi sağlamasıdır. Yani mesajın kaybolmayacağını biliriz. Ancak Relay, mesajı broker'a gönderdikten sonra DB'den silmeden çökerse, yeniden başladığında aynı mesajı bir daha gönderebilir. Bu da bizi bir sonraki probleme götürür.
Outbox Pattern Gerçek Hayatta: E-Ticaret ve Finans Senaryoları
- E-Ticaret Platformu: Bir sipariş (
Order) oluşturulduğunda,Order Servicebu bilgiyi kendi veritabanına yazar veoutboxtablosuna birOrderCreatedolayı ekler. Bu olay daha sonraInventory Service(stok düşürmek için),Notification Service(kullanıcıya e-posta göndermek için) veShipping Service(kargo sürecini başlatmak için) tarafından tüketilir.Notification Serviceo an çalışmıyor olsa bile, ayağa kalktığında mesajı alıp işleyebilir. - Finansal İşlemler: Bir banka uygulamasında, bir hesaptan diğerine para transferi (
MoneyTransfer) gerçekleştiğinde, işlemTransaction Servicetarafından veritabanına yazılır. Aynı andaoutboxtablosunaTransactionCompletedolayı eklenir. Bu olay,Fraud Detection Service(dolandırıcılık kontrolü) veStatement Service(hesap özeti oluşturma) gibi sistemleri tetikler.
2. Inbox Pattern: Alıcı Tarafının Kalkanı (Idempotency)
Outbox ile mesajın kuyruğa güvenle ulaştığından emin olduk. Şimdi masanın diğer tarafına, yani alıcı servise (Inventory Service) geçelim.
RabbitMQ gibi sistemler, bir mesajın işlendiğine dair onay (ACK) almazsa, o mesajı tekrar gönderirler (Redelivery). Eğer Inventory Service mesajı alıp stoğu düştükten sonra ACK gönderemeden çökerse, mesaj tekrar gelir ve stok ikinci kez düşebilir. Bu bir felakettir.
İşte burada devreye Inbox Pattern girer ve bize Idempotency (aynı işlemin birden fazla kez yapılsa da sonucun değişmemesi) yeteneği kazandırır.
Fikir basittir: Gelen her mesajı işlemeden önce, "Ben bu mesajı daha önce işledim mi?" diye kendi veritabanımızdaki bir inbox tablosuna sorarız.
@RabbitListener(queues = "order.created.queue")
@Transactional
public void handleOrderCreated(OrderCreatedEvent event) {
// 1. Idempotency Kontrolü: Bu mesaj daha önce işlendi mi?
if (inboxRepository.existsByMessageId(event.getMessageId())) {
log.info("Mesaj zaten işlenmiş: {}", event.getMessageId());
return; // İşlem yapma, metot hatasız bittiği için RabbitMQ'ya ACK gönderilir.
}
// 2. Asıl İşi Yap: Stoğu düşür.
inventoryService.decreaseStock(event.getOrderId());
// 3. Mesajı işlendi olarak Inbox tablosuna kaydet.
inboxRepository.save(new InboxEntity(event.getMessageId(), LocalDateTime.now()));
}
Bu kod bloğu, servisinizi kurşun geçirmez yapar. existsByMessageId kontrolü sayesinde, RabbitMQ aynı OrderCreated olayını 50 kere de gönderse, stok sadece bir kere güncellenir.
Idempotency Neden Hayat Kurtarır?
Kafka, RabbitMQ gibi modern mesajlaşma sistemleri genellikle "en az bir kez teslimat" (at-least-once delivery) garantisi verir. "Tam olarak bir kez" (exactly-once) garantisi sunmak, dağıtık sistemlerin doğası gereği (CAP Teoremi) son derece zordur ve performanstan ödün vermeyi gerektirir. Ağdaki bir kesinti sırasında, broker mesajı tüketiciye gönderip göndermediğinden emin olamazsa, en güvenli yol mesajı tekrar göndermektir.
Inbox pattern'ı bu durumu zarafetle çözer. Tüketici tarafında "tam olarak bir kez işleme" mantığını, veritabanının atomiklik gücünü kullanarak bizim uygulamamızı sağlar.
Inbox Tablosunun Anatomisi
Tıpkı Outbox gibi, Inbox deseni de gücünü basit ama etkili bir veritabanı tablosundan alır. Bu tablo, gelen her mesajın kaydını tutarak mükerrer işlemeyi engeller. Etkili bir inbox tablosu genellikle şu alanları içerir:
message_id: Gelen her mesajın benzersiz kimliği. Bu, genellikle mesajın kendisinden (örneğin, bir header veya payload içindeki bir alan) alınır ve birincil anahtar (primary key) olarak ayarlanır. Bu sayede aynı ID'ye sahip ikinci bir mesaj veritabanı seviyesinde engellenir.status: Mesajın işlenme durumunu gösterir (örn:RECEIVED,PROCESSED,FAILED). Bu, özellikle karmaşık ve çok adımlı işlemlerde hata ayıklama için kullanışlıdır.received_at: Mesajın ilk alındığı zaman damgası. Sistemdeki gecikmeleri ve performansı izlemek için önemlidir.
Bu basit tablo, alıcı servisinizi "en az bir kez teslimat" senaryolarına karşı kurşun geçirmez hale getirir. Bir mesajın işlenip işlenmediğine dair kesin bir "doğruluk kaynağı" (source of truth) oluşturur ve tüm sistemi daha öngörülebilir ve güvenilir kılar.

Böylece Outbox ve Inbox desenlerini bir araya getirdiğimizde, servisler arasında hem gönderenin hem de alıcının veritabanlarını birer güvence olarak kullanarak son derece dayanıklı bir iletişim kanalı kurmuş oluruz. Ancak bu dayanıklılık, beraberinde bazı mimari ödünleşimleri de getirir.
Bu Desenlerin Avantajları ve Dezavantajları
Bu desenler sihirli bir değnek değildir ve kendi getirdiği ödünleşimler (trade-offs) vardır.
Avantajları
- Gerçek Asenkron ve Gevşek Bağlılık (Decoupling): Gönderici servis, alıcı servislerin ayakta olup olmadığını umursamaz. Mesajını
outbox'a bırakır ve kendi işine devam eder. Bu, servisler arasında gerçek bir bağımsızlık sağlar. - Yüksek Dayanıklılık (Resilience): Tüketici servislerden biri çöktüğünde, sistemin geneli çalışmaya devam eder. Servis ayağa kalktığında mesaj kuyruğunda onu bekleyen işleri işlemeye başlar.
- Denetim ve Takip (Audit Trail):
outboxveinboxtabloları, sistemde hangi olayların ne zaman üretilip ne zaman işlendiğine dair doğal bir denetim kaydı tutar. Hata ayıklama ve takip için paha biçilmezdir.
Zorlukları ve Dezavantajları
- Artan Karmaşıklık: En büyük dezavantajıdır. Her servis için ek
outbox/inboxtabloları, entity'ler, repository'ler ve Relay (Polling/CDC) mekanizması kurmak gerekir. - Performans ve Gecikme: Mesajlar artık anında gönderilmez. Polling mekanizması kullanılıyorsa, mesajın kuyruğa ulaşması için geçen sürede küçük bir gecikme (latency) olur.
- Depolama Maliyeti:
outboxveinboxtabloları zamanla büyüyebilir. Bu tabloları periyodik olarak temizleyecek veya arşivleyecek bir stratejiye ihtiyaç duyulur. - İzleme (Monitoring): Artık sadece mesaj kuyruğunu değil, aynı zamanda
outboxtablosundaki gönderilmemiş mesaj sayısını ve Relay mekanizmasının sağlığını da izlemeniz gerekir.

Son Söz: Karmaşıklığı Kucaklamak ve Kaosu Yönetmek
Dağıtık sistemler tasarlamak, doğası gereği belirsizliklerle dolu bir yolculuktur. Servisler çökebilir, ağlar yavaşlayabilir ve mesajlar gecikebilir. Bu yolculuğun en başında, "dual write" gibi basit görünen kestirme yollar cazip gelebilir. Ancak tecrübeyle sabittir ki, dağıtık sistemlerde "mutlak tutarlılık" (strong consistency) peşinde koşmak, çoğu zaman hem pahalı hem de kırılgan bir hayaldir.
Asıl ustalık, bu gerçeği kabul edip "nihai tutarlılık" (eventual consistency) ilkesini benimseyerek, sistemlerimizi bu kaçınılmaz hatalara karşı dayanıklı hale getirmekte yatar. Outbox ve Inbox pattern'ları, bu felsefenin en somut ve güçlü uygulamalarındandır. Onlar sadece birer kodlama tekniği değil, aynı zamanda bilinçli bir mimari karardır.
Evet, bu desenler sisteme ek tablolar, yeni bileşenler ve bir miktar karmaşıklık getirir. Bu, ödenmesi gereken bir bedeldir. Ancak bu bedeli, bir "maliyet" olarak değil, bir "yatırım" olarak görmek gerekir. Bu, sisteminizin gelecekteki potansiyel veri bozulmalarına, müşteri şikayetlerine ve acil durum müdahalelerine karşı sigortasıdır.
Cuma akşamı production ortamında bir ağ dalgalanması yüzünden binlerce siparişin durumunun bozulduğunu görüp hafta sonunu kriz masasında geçirmek yerine, bu desenleri baştan kurgulayıp sistemin kendi kendini iyileştirmesini (self-healing) izlemenin verdiği profesyonel tatmin paha biçilemez.
Sonuç olarak, Outbox ve Inbox pattern'ları bize şunu öğretir: Dağıtık sistemlerde kaostan kaçamazsınız, ama onu yönetebilirsiniz. Güvenilir sistemler, her şeyin yolunda gideceği varsayımıyla değil, yolunda gitmeyeceği gerçeğiyle inşa edilir.
