Transactional Outbox Pattern: Neden ve Nasıl?

Furkan Karaoğlu
ÇSTech
Published in
5 min readNov 5, 2021

Bu yazıda “transactional outbox pattern”a neden ihtiyaç duyduğumuzu ve nasıl implemente edebileceğimizi örnek bir .NET 5 projesi ile anlattım.

Örnek projeyi bu linkte bulabilirsiniz: https://github.com/karaoglufurkan/outbox-pattern-sample-project

Mikroservis mimarisi’nde servislerin birbirine olan bağımlılığını minimize edebilmek (loose coupling), mimarinin nimetlerinden doğru bir şekilde faydalanmak için uygulanması gereken önemli noktalardan biri.

Gevşek bağlı (loosely coupled) servisler ile;

  • Bir servisin çalışmadığı durumda diğer servislerin çalışmaya devam edebilmesi,
  • Bir serviste yapılan değişikliğin diğer servisleri etkilememesi,
  • Servislerin kolay bir şekilde test edilebilmesi,
  • Ölçeklemenin kolaylaşması

gibi kritik faydalar elde ederiz.

Olay güdümlü mimari (event-driven architecture), loosely coupled servisler geliştirmemiz için kullandığımız yöntemlerden birisi.

Yöntem, sistemde yapılan değişikliklerin olaylar (events) şeklinde ortak bir platforma, bir message broker’a, yayımlanıp bu değişikliklerin tüketilmesi üzerine kurulu. X servisindeki bir işlem, Y servisini çağıracağına, yaptığı işlemi message broker’da yayımladıktan sonra (publishing), bu olaya abone olan Y servisi (subscriber), olayı yakalayıp yapması gereken işi yapıyor (consuming). Bu sayede bu iki servisin birbirine olan bağımlılığını ortadan kaldırmış oluyoruz.

Fakat burada şöyle bir sorunla karşılaşabiliriz: Eğer X servisi kendi tarafındaki işleri tamamladıktan sonra Y’nin yapması gereken iş için gerekli event’i yollayamaz ise, mesela message broker servisi ayakta değilse, işlem yarım kalmış, veri bütünlüğü sağlanamamış olur.

İşte burada yazımızın konusu olan “transactional outbox pattern” devreye giriyor. Bu pattern ile çok basit bir ifade ile X servisinin yaptığı işlem ve göndereceği event bir transactional bütünlük içerisinde gerçekleştiriliyor. Eğer event gönderilirken bir sorun ile karşılaşılır ise tüm işlem geri sarılıyor. Bu sayede veri bütünlüğünü sağlamış oluyoruz. Ayrıca artık X servisinin event’i yayımlayacağı platforma olan bağımlılığı da ortadan kaldırılmış oluyor. Message broker’ın ayakta olup olmadığı artık X’i ilgilendirmiyor.

Peki bunu nasıl yapıyoruz?

Yukarıdaki diyagramda da görüldüğü gibi, X servisi gerekli veri tabanı işlemlerini yaptıktan sonra direkt olarak message broker ile iletişime geçmek yerine event’lerin tutulduğu başka bir tabloya kayıt atıyor. Bu şekilde X servisi tüm bu işlemi bir transactional bütünlükte yapabiliyor. Diğer yandan ise bir message relay, tabloya yazılan bu event’leri alıp message broker’a yolluyor. İşlem başarılı ise event’lerin gönderildiğine dair tablodaki kayıtları güncelliyor.

Bu durumun daha iyi oturması için örnek bir proje geliştirdim. Gelin ona bir göz atalım:

Order Service ve Mail Service adında iki adet servisimiz var. Temelde amacımız; Order Service’te bir sipariş yaratma ya da iptal durumunda message broker’a ilgili event’leri yollamak. Bu sayede MailService abone olduğu bu eventleri yakalayıp MailQueue tablosunda siparişin yaratıldığına/iptal edildiğine dair bir mail kaydı atacak. Tabi bu işlemi “transactional outbox pattern”i uygulayarak yapacağız.

Öncelikle tablolarımıza göz atalım:

Burada asıl odaklanmamız gereken tablo OutboxEvents. “Data” alanında ilgili event’e dair veri tutuluyor, “Type” alanında ise event’in tipi. “State” alanı ise bize event’in hangi durumda olduğunu söylüyor (ReadyToSend, SentToQueue vs..). Message relay (dispatcher), hangi kayıtlar için event yayımlayacağını bu state’e göre belirleyecek.

Aşağıdaki ekran görüntüsünde OutboxEvent modelinin nasıl oluşturulduğunu görebilirsiniz:

Shared/Models/OutboxEvent.cs

OrderService’te, bir transaction içerisinde nasıl sipariş ve event kayıtlarının atıldığının bir örneği:

Publisher ve consumer’ı anlatmadan önce RabbitMQ ve MassTransit’ten bahsetmem gerekiyor.

RabbitMQ, AMQP protokolünü kullanan, açık kaynak kodlu bir message broker yazılımı. Örnek projemizde bunu kullanıyoruz.

MassTransit ise .NET’te RabbitMQ gibi mesage broker’ların karmaşık yapısını soyutlayarak message-based, loosely-coupled sistemler inşaa etmeyi kolaylaştıran açık kaynak kodlu bir framework.

MassTransit sayesinde RabbitMQ’da ilgili exchange ve queue’ları manuel olarak oluşturmamıza gerek kalmıyor. Kendisi bizim oluşturduğumuz event sınıflarının isimlerine göre gerekli yapıları bizim için kuruyor. Bunu sağlayabilmek için bu sınıfları servislerin referans alacağı ortak bir kütüphanede bulunduruyoruz ve işlemlerimizi bu sınıflar üzerinden yapıyoruz.

Ortak sınıf kütüphanesinde event’ler için oluşturulan sınıflar

Şimdi sıra geldi OrderService tarafından oluşturulan event kayıtlarını yayımlamaya. Bunun için belirli periyotlarla polling yaparak gönderilmemiş event’leri alıp ilgili queue’lara gönderen bir worker service oluşturuyoruz. Bunun için Quartz.NET isimli bir kütüphane kullandım. DI container’a nasıl register edildiği ile alakalı detayları proje kodunda bulabilirsiniz.

Dispatcher/Jobs/OutboxJob.cs

Quartz.NET’in sağladığı “IJob” interface’ini implemente ederek bir “OutboxJob” sınıfı oluşturduk. Bu sınıfın “Execute” metodu belirlediğimiz periyotlarla tabloyu kontrol ederek yayımlanmamış event’leri message-broker’a gönderip sonrasında ilgili kayıtların state’lerini güncelliyor.

Burada aklınıza şöyle bir soru takılabilir: Event yayımlandıktan sonra metotta bir hata alınırsa ne olacak? Böyle bir durumda aynı event birden fazla kez yayımlanmış olmaz mı? Evet böyle bir risk var ve hatta bu durumu önleyebileceğimiz yöntemler de. Bkz: Idempotent Consumer. Fakat asıl konudan fazla sapmamak için buna bu yazıda değinmeyeceğim.

Event’leri de yayımladıktan sonra son bir işimiz kalıyor. O da bu event’leri consume eden MailService’i oluşturmak.

MailService/Consumers/OrderCreatedConsumer.cs

MassTransit’in “IConsumer<T>” interface’ini implemente ederek yayımlanan event’in nasıl kullanılacağını “Consume” metodu içerisinde belirtiyoruz. Burada bu metot basit bir şekilde gelen event’e göre “MailQueue” tablosuna bir mail kaydı atıyor.

“docker-compose up” komutu ile sistemimizi ayağa kaldırdıktan sonra artık test edebiliriz. Öncelikle Postman üzerinden bir sipariş yaratalım:

GET Orders yaptığımızda siparişin oluştuğunu görebiliriz:

Bakalım siparişin yaratıldığına dair oluşturulan event yerine ulaşmış mı:

OutboxEvents tablosundaki kayıtları kontrol edelim:

RabbitMQ Management ekranına erişerek MassTransit tarafından otomatik olarak oluşturulan exchange ve queue’ları görebiliriz:

Görünüşe göre sistemimiz düzgün bir şekilde çalışıyor.

Detaylar için projeyi incelemenizi tavsiye ederim. Vakit ayırdığınız için teşekkür ederim. Umarım bir nebze faydalı olmuştur. Görüşmek üzere…

--

--