DDD ve Mikroservis Mimari

Domain Driven Design'ın Bounded Context Kavramı ve Mikroservis Mimari

Bir önceki bölümde bahsettiğimiz iletişim yöntemlerinden olan asenkron iletişimde event-based mimari uygulansa bile servisler veri paylaşımı için birbirlerine anlık olarak http istekleri attıkları için "bağımsızlık" prensibine aykırı bir durum oluştuğundan kısaca bahsetmiştik. Bu bölümde bu http isteklerinden de kurtularak daha izole ve bağımsız servislere nasıl ulaşırız sorusunun yanıtını arayacağız.

Konuyu aşağıdaki alt başlıklarda ele alarak anlatmaya çalışacağım. Bu bölümde esas değinmek istediğim konu son madde, yani veri paylaşımı konusu. Bu önemli konuyu örnek bir senaryo üzerinden inceleyeceğiz. Önceki bölümleri bir ön hazırlık olması açısından eklemenin iyi olacağını düşündüm.

  • Mikroservis Mimari ve Domain-Driven Design

  • Bounded Context nedir?

  • Her Bounded Context bir Mikroservis anlamına gelir mi?

  • Bounded Context’ler (Mikroservis’ler) arası veri paylaşımı

Mikroservis Mimari & DDD

Mikroservis Mimari ve DDD gibi iki ağır konuyu bir başlık altına sığdırmaya çalışmayacağız elbette, ki biraz iddialı bir hedef olurdu bu.

Bu bölümde, bu iki mimari arasındaki ilişkiyi birlikte kullanılabilirlik yönünden inceleyemeye çalışacağız.

  • Birlikte Kullanmaktan Kastımız Nedir?

Ekip olarak yeni ve uzun soluklu bir projeye başlıyorsunuz ve proje belirli bir olgunluk seviyesine ulaştıktan sonra monolith den Mikroservis mimariye dönüştürmeyi planlıyorsunuz diyelim. Hedefte kesin olarak Mikroservis mimari olmasına rağmen, başlarken monolith yapıda başlayarak devam ediyorsunuz ki bence de böyle yapmalısınız. ( Monolith mi yoksa Mikroservis mi başlamanın daha doğru olduğu sorusu bu yazsının konusu olmadığından burada üzerinde durmadan devam edelim. )

Monolith yapınızı kurgularken aldığınız teknik kararların, ileride Mikroservis Mimari’ye dönüşüm sürecinizin zorluk seviyesini belirleyeceğini unutmamalısınız. Tam bu noktada DDD’den bahsedebiliriz. Monolith yapıda başladığımız projede DDD’yi prensiplerine sadık kalarak doğru bir şekilde uygularsanız, bu dönüşüm işlemini hem daha kolay hem de daha doğru ve daha az taviz vererek yapabilirsiniz. Bu avantajı bize sağlayacak olan ve bir sonraki bölümde bahsedeceğimiz kavram DDD’nin Bounded Context kavramı.

Bu arada, monolith mimarinizde DDD uygulamazsanız Mikroservis Mimari dönüşümü yapılamaz gibi bir mesaj vermeye çalışmıyorum. Amacımız gevşek bağlı bir mimari oluşturmak ve DDD’nin de burada bize yardımcı olabileceğinden bahsediyorum aslında. Yani DDD buradaki tek alternatifimizi değil elbette.

Mesela, son zamanlarda adını biraz daha fazla duymaya başladığım Modular Monolith tasarımdan da çok kısa bahsetmek isterim. Aslında bu mimari için ismiyle müsemma demek yanlış olmayacaktır. Aşina olduğumuz monolith mimarinin modüler, yani bağımlılıklardan mümkün olduğunca arındırılmış bir tarzda kurgulanması diyebiliriz. Konuyla ilgili burada güzel bir makale mevcut bir göz atmanızı tavsiye ederim. Yine aynı makale yazarının, DDD ve Moduler Monolith mimariyi birlikte uygulayarak geliştirdiği, incelemeye değer gördüğüm bir projeye de buradan ulaşabilirsiniz.

Bounded Context Nedir?

Bounded Context, DDD’nin anlaşılması biraz zaman alan ve aynı zamanda da en önemli kavramlarından birisidir. Burada “zor” ile kastettiğim şey; Bir Bounded Context’in sınırlarının belirlenmesi konusu. Aynı zorluk bir Aggregate’in tanımlanması için de geçerli diyebilirim.

Domain’im de kaç tane Bounded Context olduğu, bunların sınırları ve birbirleriyle hangi noktalarda ilişkili oldukları konuları kritik önem arz ediyor.

  • Bounded Context’leri Keşfedilmesi

Bounded Context’leri domain expert’ler ile konuşarak ve bazı ip uçlarından faydalanarak ortaya çıkarmalıyız. Bu ip uçlarına gelmeden önce belirtmekte fayda var; Bounded Context leri bir kere belirledim, artık değişmez gibi bir düşünceye kapılmamalıyız. Domain expert’lerle konuşarak ve değişen şartları da göz önüne alarak çizdiğiniz sınırlarda değişiklikler yapmanız, Bounded Context’lerinizi yeniden şekillendirmeniz gerekecektir.

( Hatta belki bu sınırlar belirginleşene kadar DDD’yi bir kenara bırakmalı ve Bounded Context’lerinizi büyük ölçüde netleştirdikten sonra uygulamanızı DDD için refactor etmelisiniz. Ancak bu refactoring sürecinin maliyeti sizin bu süreci başlatma zamanınıza göre belirleneceği için elinizi çabuk tutmanız gerekebilir. )

Bir Bounded Context’in sınırlarını belirlerken domain expert’ler ile doğrudan konuşma dışında hangi ip uçlarından faydalanabiliriz biraz bunlara değinelim.

Kullanıldığı domain’e göre farklı anlamlara gelebilen kavramları bulmaya çalışın. Örneğin ‘x’ kelimesi kullanıldığı yere göre 2 farklı anlama bürünüyorsa, bu 2 farklı Bounded Context’in varlığını işaret ediyor olabilir. Örneğin; Product kelimesi, Shipment Context’in de ağırlığı olan taşınacak bir yük anlamına gelirken, Inventory Context’in de elde kaç adet bulunduğu veya mevcut olup olmadığı önemli olan bir sayıdan ibaret aslında.

Diğer bir ip ucu olarak; Bounded Context’lerinizi tanımladınız ve geliştirme süreciniz devam ediyor diyelim. Ancak bir sorun var, bir context’de ki bir veriyi değiştirdiğinizde başka bir context’te de bir veri değiştirmek zorunda kalıyorsunuz. Bu iki context’in birbirine bağımlı olduğu anlamına geliyor. Daha da kötüsü bu durum farklı farklı veriler için sıkça yaşanmaya başlıyor. Bu durumda, bu ikisi context’in tek bir context altında birleşmesi durumunu değerlendirmemiz gerekiyor.

Son olarak, DDD’de yer alan Aggregate Root, Entity, Value object, Domain Event gibi tanımlamaları en doğru şekilde yapabilmemiz için, Bounded Context’lerimizi de en doğru şekilde tanımlamamız gerektiğini bilmemiz gerekiyor.

  • Neden ‘Bounded’ Context?

Eric Evans’ın kitabının kapağında da belirttiği gibi, DDD’nin karmaşık domainlerde, yazılımın merkezinde yer alan o karmaşıklıkla mücadele ettiğini biliyoruz.

Yazılımda karmaşıklık ve bu karmaşıklıkla başa çıkabilme konularına baktığınızda önünüze ilk çıkan şeylerden birisi loosely coupled (gevşek bağlı) bir tasarımın gerekliliği olur. Burada birbirine bağlı olmayan veya çok az bağımlı olmasını istediğimiz bu parçacıkları genelde module olarak isimlendiriyoruz. Modüler tasarım ifadesini çokça duymuşsunuzdur. DDD’de birbirine bağımlı olmaması gereken bu modüller bounded yani sınırlı context’ler olarak isimlendiriliyor.

Gerçek dünyada domain’lerin kesin hatlarla belirli olmayan sınırları vardır. Bazı noktalarda benzerlik gösteren, ortak kavramlar barındıran domain’ler olabilir. Yukarıda bahsettiğimiz Product kavramının hem Product hem de Shipment context leri için anlamlı olması örneğindeki gibi, Product ve Shipment domain lerini kesin hatlarla ayıramıyoruz.

Ancak yazılım Dünyasında bu gevşek bağlı mimariyi kurgulayabilmemiz için sınırları kesin olarak belirli olan modüllere ihtiyacımız vardır. DDD ye göre, Product kelimesinin Product Context’inde ki anlamıyla Shipment Context’indeki anlamı farklıdır. Yani hangi context sınırları içerisindeyse ona göre anlam kazanır. Bounded ifadesinin bu duruma vurgu yapmak için kullanıldığını düşünüyorum.

Bir Bounded Context == Bir Mikroservis ?

Bu sorunun her koşulda doğru olan bir cevabı yoktur diyebiliriz.(Yazılım mimarilerinde birçok konuda olduğu gibi)

Bir Mikroservis, bir Bounded Context’i veya onun bir bölümünü temsil edebilir. Diğer bir deyişle, bir Bounded Context birden fazla Mikroservis’te doğurabilir. Bu tamamen, söz konusu Mikroservis’in ölçeklenebilme ve bağımsız hareket edebilme gereksinimine bağlı olarak verilecek bir karardır aslında. Bunlar esasında birbirine benzer iki kavram olmakla beraber ;

Bir Bounded Context bize domain’in sınırlarını çizerken, bir Mikroservis, domain’den etkilenmekle beraber teknik ve organizasyonel sınırları belirler.

Özetlersek, DDD ile geliştirdiğimiz monolith uygulamamızın Mikroservis Mimari’ye dönüşümü yaparken, “Her Bounded Context için bir ve yalnızca bir Mikroservis oluşturmalıyız” gibi bir kalıbın içine girmemiz yanlış olacaktır diyebiliriz.

Bounded Context’ler (Mikroservis’ler) Arası Veri Paylaşımı

Evet geldik bu bölümde esas bahsetmek istediğimiz konuya. Buraya kadar olan kısım aslında bu bölüme bir ön hazırlıktı diyebiliriz . Başlamadan önce buradaki esas amacımızı tek cümleyle özetlemek gerekirse ;

Birbirlerinin verisine ihtiyaç duyan servislerimizin bu ihtiyacını, servisleri birbirlerine bağımlı hale getirmeden, sınırları(boundaries) ihlal etmeden giderebilmek, şeklinde ifade edebiliriz.

Bu bölümde 3 örnek Bounded Context arasında ki ilişkili noktaları ve bu ilişkinin getirdiği veri paylaşımı zorunluluğunu asenkron olarak çözmemizi sağlayan bir yöntemden bahsedeceğim. Bu yöntemle alakalı Julie Lerman'ın buradaki yazısını incelemenizi tavsiye ederim.

Meseleyi 3 adet basitleştirilmiş Bounded Context üzerinden ele alalım. Bunlar Customer, Product ve Discount context’leri olsun. Customer müşteri ile alakalı iş kurallarını içerirken, Product ürün bilgileri ve Discount satılan ürünler için indirim uygulama iş kurallarını içeriyor.

Bounded Context’lerimizi ve birbiriyle ilişkili oldukları noktaları aşağıdaki gibi göstermeye çalıştım.

Dikkat ettiyseniz Discount context’i hem Product hem de Customer ile ilişkili durumda. Bir diğer deyişle Discount bu iki context in verisine ihtiyaç duymakta. Yuvarlak içerisinde belirttiğim diğer konseptler ise sadece o context içerisinde bir anlamı olan ve diğer context leri ilgilendirmeyen entity’ler.

Bir örnek vermek gerekirse, Product context i içerisinde ProductCategory adında bir entity daha var. Discount servisi indirim uygularken ürünün kategori bilgisine de ihtiyaç duysaydı bu category entity sini de ilişkili olarak göstermemiz gerekecekti. Ancak bu örnek senaryomuzda Discount servisinin müşterinin tipine göre bir ürüne indirim uygulaması isteniyor.

Örneğin bazı premium müşterilere, aynı gün yaptıkları ikinci alış verişte %50 indirim uygulanması gibi bir iş kuralımızın olduğunu düşünelim. Bu durumda Discount servisimiz, hem ürünün Id ve Price bilgisine hem de müşterinin Id ve CustomerType bilgisine ihtiyaç duymaktadır. Bu 4 bilgi haricindeki diğer bilgilerle ilgilenmediğini vurgulayarak devam edelim.

Eğer monolith yapıda ve bir tek ilişkisel veri tabanına sahip bir uygulamamız olsaydı kabaca aşağıdaki gibi tasarlayabilirdik.

Discount işlemi uygulanırken Discount tablosuna, “X ürünü için Y müşterisine tanımlı bir indirim var mı?” sorgusuyla gelerek süreci yönetebiliriz.

DDD’ye geri dönersek, biz context’lerimizi birbirinden izole ederek otonom bir yapıya bürünmelerini istiyoruz ve aradaki iletişimin event-based, yani asenkron olmasını istiyoruz. Peki bu durumda Discount servisi indirim uygularken Customer ve Product verisine ihtiyaç duyduğu anda nasıl bir yol izlemeli? CustomerId’yi kullanarak CustomerType bilgisine nasıl ulaşmalı? Customer Service’e, http isteği yaparak bu ihtiyacını pek tabi giderebilir ancak daha öncede söylediğimiz gibi bu servislerimizi birbirine bağımlı hale getirdiğinden biz bunu istemiyoruz. (Örneğin, buradaki 40. satırda oluşan bağımlılık gibi.)

Peki ne yapmak lazım?

Discount servis, Product ve Customer servislerinden sadece ihtiyacı olan verilerin read-only bir kopyasını kendi veri tabanında saklarsa nasıl olur? Buna uygun olan yeni veri tabanı yapımız aşağıdaki gibi şekillenecektir. Dikkat ederseniz bir ilişkisel veri tabanımız varken artık 3 izole veri tabanına sahibiz. Bu veri tabanları sql/nosql/graph vb. her hangi bir tipte olabilir. Bu şuan konumuz dışında.

Not: DDD’de her Bounded Context için ayrı ve izole bir veri tabanı olması şartı yoktur. Aynı veri tabanında şema bazlı bir ayrıma da gidilebilir. (Product.Product, Discount.Product, Discount.Customer gibi.)

Discount servisin veri tabanında customer ve product tablolarının read-only kopyalarının olması gerektiğini belirtmiştik. Burada read-only den kastımızı biraz açalım.

Sisteme yeni bir ürün eklendiğinde Product service, ProductCreated Domain Event’ini fırlatır. Bu event’i dinleyen Discount Service (discount service yerine sadece bu işi yapmakla sorumlu başka bir service olması daha doğru olur ) event’i yakalayarak Discount servisin veri tabanındaki Product tablosuna bu ürünü ekler. Ancak dikkat ettiyseniz bu tabloda sadece 2 alan mevcut, Id ve Price. Daha önce belirttiğimiz gibi burada tüm product verisini tutmamıza gerek yok, sadece Discount Context’i için anlamlı olan ürün verisini saklıyoruz.

Bu product tablosuna event harici başka bir yolla veri yazma ve silme işlemi kesinlikle yapılmamalıdır. Yani, ProductCreated, ProductDeleted, ProductUpdated, ProductDeactivated vb. gibi Domain Event’lerin oluşması haricinde hiçbir şekilde bu tablo üzerinde bir değişiklik yapılmamalıdır diyebiliriz. Read-only den kastımız buydu aslında.

Eğer bu yöntemi ilk kez duyduysanız şuan kendinize şunu soruyor olmalısınız; “Discount veri tabanında ki bu Product ve Customer kopya tablolarının esas veri kaynağıyla olan senkronizasyonundan nasıl emin olacağız? Başımıza iş almıyor muyuz?” Evet alıyoruz aslında.

Sisteme yeni bir ürün eklendiğinde bu ürün discount service’in Product tablosuna eklenemezse ne olacak? Veri tabanına yazma işlemi sırasında bir hata meydana gelebileceği gibi, ilgili event bus’a hiç gönderilememiş bile olabilir. Event-Driven mimari ile uğraştıysanız kaybolmuş, akıbeti meçhul olan event problemiyle karşılaşmışsınızdır. Can sıkıcı olabiliyor.

Discount servisimizin, aslında sistemde mevcut olan bir ürün için, “Böyle bir ürün yoktur.” şeklinde bir hata dönmesini istemeyiz. Açıkçası bu yöntemin en kritik noktası işte bu veri tutarlılığını sağlayabilmek. Bunun için bazı yöntemlerden bahsedeceğiz ancak bahsetmeden önce kişisel tavsiyem olarak şunu söyleyebilirim.

Eğer Discount servis kendi read-only Product tablosuna erişir ve ilgili veriyi bulamazsa, verinin gerçek sahibi olan Product servise anlık http isteği ile erişerek bir de oradan sorgulama yapabilir. Sorgu sonucu 2 ihtimallidir. Ürün yoksa, sorun da yok. Ancak ürün varsa, bu aradaki senkronizasyonun bozulduğu anlamına gelir. Bu durumda Discount service ürün bilgisine verinin ana kaynağından eriştiği için çalışmasına devam edebilir ve buradaki senkronizasyon bozukluğunu size bildirmek için bir event fırlatabilir.

Yani, ”Ben x id’li ürünü kendi veri tabanımda bulamadım ama Product servise sorduğumda bana var olduğunu söyledi. Hayırdır?” anlamına gelen bir event’den bahsediyorum. Bu gibi event’leri dinleyen “Repair” rolünde ki farklı bir servis, bu x id’li ürünün Discount servisin Product tablosuna eklenmesini sağlayabilir. Tüm servisler bu beklenmedik durum için “Repair” servisine böyle bir event gönderebilirler.

Normal şartlarda, sistemde mevcut olmayan bir product id için discount servise bir indirim talebiyle gelinmesini beklemeyiz. Ancak aradaki senkronizasyonun bozulduğu durumlarda, önerdiğim yöntemle Discount servis çalışmasına devam edebilecek. Bu yöntemi uygulayarak servisleri birbirine bağımlı hale getirdiğimiz düşüncesine kapılmayın, çünkü Discount servis her işlemde yine ilk olarak kendi read-only tablolarına(product, customer) bakacak ve sadece çok nadir olmasını beklediğimiz senkronizasyon sorunlarında http isteği atarak ilerleyecek ve repair servisi haberdar edecek.

Kişisel önerimden bahsettiğime göre, veri tutarlılığını sağlayabilmek için uygulayabileceğimiz yöntemlerden bir kaçını çok detaya girmeden açıklayalım. Bu arada bu yöntemler, bu yazıda bahsettiğimiz veri paylaşımı metodunu olduğu kadar, tüm event-driven mimarileri ilgilendiren data consistency sorununun çözümü için geçerlidir.

  • Repair Service

Yukarıda, hangi yöntemi uygularsanız uygulayın ekstra bir güvenlik önlemi olarak düşünebileceğiniz bir öneriden bahsederken değinmiş olduk aslında. Biraz daha açmak gerekirse, bütün işi read-only tabloların veri tutarlılığını sağlamak olan bir servis oluşturabiliriz. Bu servis her tetiklendiğinde esas veri kaynağı ile read-only tablolarının eşitlenmesi işini icra edecek. Bu eşitleme işlemini farklı yollarla yapabilir.

  • Domain Event’lerin Persistent(kalıcı) olarak saklanması

Event Sourcing yönteminde, event doğrudan bir bus yerine bir stream veya NoSQL veri tabanına yazılarak kalıcı olması sağlanır. Her listener servisi kendi veri tabanında bu event’lerin durumunu takip eder.

  • Outbox Pattern

Bu yöntemde Domain Event’ler yine doğrudan bir bus’a yazılmıyor. Bunun yerine event’i fırlatan servisin kendi veri tabanında “outbox” rolündeki bir tabloya yazılıyor. Ancak burada kritik olan nokta, event’den önce yapılan işlemin ve outbox tablosuna yazılan event’in aynı transaction’ın bir parçası olması. Yani, sisteme yeni bir ürün eklendiğinde, ürün ekleme işlemi ve ProductCreated event’inin outbox tablosuna yazılması işlemi aynı transaction’da yapılarak event’in db’ye kaydedilmesi garantileniyor.

İkinci aşama ise, outbox tablosuna yazılan bu event’lerin bağımsız bir servis tarafından alınarak event bus’a yazılmasıdır. Bu bağımsız servis bu işlemi bir kaç farklı metotla yapabilir. Konuyla ilgili Chris Richardson'ın burada ve burada bahsettiği yöntemleri inceleyebilirsiniz.

  • Retry Policy

Publisher servis tüm listener servislerden haberdardır ve her bir event’in ilgili listener’a başarılı bir şekilde iletildiğinden emin olur. Bu başarılı gönderim bilgisi “acknowledgement” olarak bilinir. Bu bilgiyi alana dek servisin event’i tekrar tekrar göndermesini sağlayabilirsiniz. Bu şekilde en azından event’in listener tarafından alındığından ve işlendiğinden emin olunur. Burada kritik nokta, listener servisin “acknowledgement” bilgisini hangi aşamada gönderdiğidir. En doğru olanı, event’i alıp ilgili işlemi (Discount servisin yeni eklenen ürünü read-only tabloya kaydetmesi gibi) başarıyla yaptıktan sonra bu bilgiyi iletmesi olacaktır.

Bunun dışında farklı yöntemlerde mevcut, her bir yöntemin diğerlerine göre artıları ve eksileri olduğunu da belirtmekte fayda var.

Sonuç

Yazılım mimarilerinde bir sorunu çözmek veya bir kazanım elde etmek için yapılan her tercih yeni zorlukları da beraberinde getiriyor. Bu neredeyse her durumda geçerli bir kural adeta. Tıpkı, yazıda bahsettiğim, bir servisin başka bir servisin ihtiyaç duyduğu verisinin read-only bir kopyasını kendi veri tabanlarında tutması yönteminde olduğu gibi. Burada kazanımımız, tamamen izole ve otonom servisler elde etmek iken (ki büyük bir kazanım), bu read-only kopyanın esas veri kaynağı ile senkronizasyonu konusu ise yeni bir zorluğu beraberinde getiriyor.

Last updated