Microservice Mimari ve DDD’nin Bounded Context Kavramı Üzerine

Suat KÖSE
Devops Türkiye☁️ 🐧 🐳 ☸️
10 min readDec 28, 2019

Bir süredir Domain-Driven Design konusuyla haşir neşir olmaktayım, bu yüzden DDD ve Microservice Mimari üzerine bir şeyler yazmak istedim. Microservice Mimari ile alakalı diğer yazılarıma profil sayfamdan göz atabilirsiniz.

Konuyu aşağıdaki alt başlıklarda ele alarak anlatmaya çalışacağım. Yazıda 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.

  • Microservice Mimari ve Domain-Driven Design
  • DDD’de ki Bounded Context nedir?
  • Her Bounded Context bir Microservice anlamına gelir mi?
  • Bounded Context’ler (Microservice’ler) arası veri paylaşımı

Microservice Mimari & DDD

Microservice 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 ve birlikte kullanılabilirliklerini değerlendirdikten sonra, Microservice Mimari’de servisler arası veri paylaşımı konusuna ufak bir giriş yapacağı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 microservice mimariye dönüştürmeyi planlıyorsunuz diyelim. Hedefte kesin olarak microservice 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 Microservice 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 Microservice 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 Microservice 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.

  • Microservice’ler Arası Veri Paylaşımı

Önceki yazılarımdan birinde Microservice’ler arası senkron ve asenkron iletişim yöntemlerinden bahsetmiştim. Asenkron iletişimde event-driven mimari uygulansa bile, servislerin anlık olarak birbirlerinin verisine ihtiyaç duyduğu senaryolarda http isteği yapmalarından dolayı bağımsız servislere sahip olamıyoruz. Bu http istekleri, microservice mimari’nin temel prensiplerinden “bağımsızlık” ilkesine aykırı bir durum aslında.

Son bölümde, bu iletişimi tamamen asenkron hale getirerek, servislerin birbirlerine hiçbir şekilde http isteği yapmadan nasıl veri alış verişinde bulunabileceklerinden bahsedeceğiz. Fakat öncesinde, DDD’nin Bounded Context kavramından ve Microservice Mimari’de neye karşılık geldiğinden bahsetmek istiyorum.

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’lerin 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 Microservice ?

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

Bir Microservice, bir Bounded Context’i veya onun bir bölümünü temsil edebilir. Diğer bir deyişle, bir Bounded Context birden fazla Microservice’te doğurabilir. Bu tamamen, söz konusu microservice’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 Microservice, domain’den etkilenmekle beraber teknik ve organizasyonel sınırları belirler.

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

Sharing Data Between Bounded-Contexts (Microservices)

Evet geldik yazıda esas bahsetmek istediğim 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 ablamızı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 Contex’lerimizi ve birbiriyle ilişkili oldukları noktaları aşağıdaki gibi göstermeye çalıştım.

Bounded Contexts

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.

Relational Database Design

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 veritabanı 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 veritabanı olması şartı yoktur. Aynı veri tabanında şema bazlı bir ayrıma da gidilebilir. (Product.Product, Discount.Product, Discount.Customer gibi.)

Isolated Databases for Each Microservice

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şte 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 abimizin 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 veritabanları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.

Bu yazıda, microservice mimari’de servisler arası bağımlığı azaltma ve dolayısıyla, daha bağımsız servisler elde edebilme yolunda kullanılabilecek bir yöntemi anlatmaya çalıştım. Eksik veya hatalı bulduğunuz noktaları ve konuyla ilgili olarak gerçek hayat deneyimlerinizi yorumlar bölümünden dinlemeyi çok isterim.

--

--

Suat KÖSE
Devops Türkiye☁️ 🐧 🐳 ☸️

A Turkish family guy. Software engineer. Minimalist. #microservices #softwaredesign #dotnetcore — github.com/suadevsuperpeer.com/suadev