Arda Akgür
Yazılım mimarisinde çoğu fikir tamamen sıfırdan doğmaz.
Bir mühendisin öğrendiği bir prensip, başka bir problemle karşılaştığında farklı bir şekle dönüşür. Bazen küçük bir fikir yıllar içinde büyür, uygulanır, kırılır, yeniden tasarlanır ve sonunda kendi mimari yaklaşımına dönüşür.
ARC-DDD’nin hikâyesi de böyle başladı.
Bu yaklaşımın ilk versiyonlarını Avustralya’da Associate Engineer olarak çalıştığım dönemde geliştirmeye başladım.
Ancak fikrin nereden geldiğini anlatırken özellikle bir kişinin hakkını teslim etmem gerekiyor:
Victorian Software CEO’su Adrian Budiman.
Adrian bana domain’leri yalnızca klasik mimari katmanlar olarak düşünmek yerine, sistemi yatay olarak ayrı domain’lere bölme fikrini gösteren ilk kişiydi.
Bu fikir benim için başlangıç noktası oldu.
Ben daha sonra bu yaklaşımı kendi geliştirdiğim sistemlerde çok daha ileri taşıdım.
Bugün ARC-DDD, yani zaman zaman kullandığım adıyla Australian DDD, bu sürecin sonucudur.
Klasik Domain-Driven Design genellikle sistemi dikey katmanlara ayırır:
Bu yaklaşım son derece değerlidir ve ARC-DDD bunu ortadan kaldırmaya çalışmaz.
Benim eklediğim şey ikinci bir eksendir.
Sistem dikey katmanlara ayrılırken domain modelinin kendisi de yatay bounded context’lere bölünür.
Örneğin:
Core├── Users├── Events├── Transactions├── Vendors└── PaymentsBuradaki temel kural şudur:
Bir domain başka bir domain’in bütün dünyasını bilmemeli.
ARC-DDD’nin asıl karakteri burada ortaya çıkar.
Büyük sistemlerde en sık gördüğüm problemlerden biri devasa entity modelleridir.
Örneğin sistemde tek bir User sınıfı vardır.
Zaman geçtikçe içine her şey eklenir:
PasswordPhoneAddressBalanceIBANKYCPermissionsProfileSecurityTransactionsPreferences...Sonra Events domain’i yalnızca kullanıcının adını ve fotoğrafını göstermek istediğinde bile bütün User modelini kullanmaya başlar.
Transactions domain’i balance ve IBAN için aynı modeli kullanır.
Vendor sistemi yine aynı modeli kullanır.
Sonuçta teoride domain’lere ayrılmış görünen sistem, gerçekte aynı entity’ler üzerinden birbirine bağlanmış dev bir yapıya dönüşür.
ARC-DDD burada farklı bir yaklaşım kullanır.
Örneğin:
Users.UserEvents.User2Transactions.User3Bunlar farklı domain modelleridir.
Ama aynı fiziksel database tablosunu temsil edebilirler.
Users.User ──┐Events.User2 ──┼──> Users tableTransactions.User3 ──┘Events domain’i yalnızca şunlara ihtiyaç duyuyorsa:
UserIdFirstNameFamilyNamePhotoUser2 sadece bunları içerir.
Transactions domain’i:
UserIdBalanceIBANVerifiedalanlarına ihtiyaç duyuyorsa User3 kendi domain ihtiyacına göre tanımlanır.
Her domain dünyayı kendi ihtiyacı kadar görür.
ARC-DDD’nin temel yaklaşımı da tam olarak budur: bounded context’ler ayrıdır ve başka bir domain’e ait aggregate’ın tamamını taşımak yerine, ihtiyaç duyulan alanları içeren domain-local projection oluşturulur. Bu projection gerektiğinde aynı fiziksel tabloya map edilir.
ARC-DDD’nin önemli özelliklerinden biri bounded context oluşturmak için mutlaka ayrı database kullanmayı zorunlu görmemesidir.
Örneğin Entity Framework Core ile:
modelBuilder .Entity<User2>() .ToTable("Users") .HasKey(x => x.UserId);Events domain’indeki User2, yine fiziksel Users tablosuna bağlanabilir.
Ama modelde yalnızca Events domain’inin ihtiyaç duyduğu property’ler bulunduğu için o domain gereksiz kullanıcı alanlarını kendi modeli içerisine taşımaz.
Public reference implementation’da (eski-dev) Goldtag örneğinde User, User2 ve User3 aynı Users tablosuna farklı domain perspektiflerinden bağlanır. User2 Events için, User3 ise Transactions için gereken veri ve davranışları temsil eder.
Bu bize önemli bir denge sağlar:
Database fiziksel olarak ortak olabilir, fakat domain modelleri ortak olmak zorunda değildir.
ARC-DDD’nin temel prensiplerinden biri budur.
Bu ayrım yalnızca klasör seviyesinde kalmaz.
Her bounded context kendi DbContext yapısına sahip olur.
Örneğin:
UserDbContextEventsDbContextTransactionDbContextVendorsDbContextEvents domain’i kendi projection modellerini bilir.
Transactions domain’i kendi modellerini bilir.
Users domain’i ise canonical User modelinin sahibidir.
Public reference implementation’da (eski-dev) örneğin UserDbContext canonical User modelini, EventsDbContext User2 projection’ını, TransactionDbContext ise User3 projection’ını kendi context’i içerisinde kullanır.
Bu nedenle bir domain başka bir domain’in Core modellerini doğrudan import etmez.
Events içinde:
using Gold.Core.Users;yapıp bütün User aggregate’ını sisteme taşımak yerine Events kendi projection’ını oluşturur.
Bu bilinçli bir tekrardır.
Çünkü ARC-DDD’de bazen:
Küçük miktarda duplication, güçlü coupling’den daha ucuzdur.
Core seviyesindeki temel kurallardan biri de domain klasörlerinin birbirlerini doğrudan import etmemesidir. Örneğin Events, Gold.Core.Users kullanmak yerine User2 gibi kendi yerel temsilini oluşturur.
Gerçek uygulamalarda bounded context’ler tamamen izole yaşayamaz.
Bir işlem aynı anda:
gibi birden fazla domain’e ihtiyaç duyabilir.
ARC-DDD burada domain katmanlarını birbirine bağlamak yerine composition işlemini sistemin dış kenarına taşır.
Örneğin:
API | +--> OrderingRepository | +--> PaymentsRepository | +--> UsersRepositoryYani cross-domain orchestration API veya application seviyesinde gerçekleşir.
Core domain’leri birbirlerini import ederek bağımlılık zinciri oluşturmaz.
eski-dev referansında da cross-domain composition API tarafında yapılır. Bir controller veya application service gerektiğinde birden fazla repository ile çalışabilir, fakat Core domain’leri birbirine bağlanmaz.
Bu ayrım sistem büyüdükçe çok değerli hale geliyor.
Bugün yeni bir ARC-DDD sistemi oluşturacak olsam temel yapıyı oldukça basit tutarım:
Product.CoreProduct.DomainProduct.ApiBağımlılık yönü:
Product.Api ↓Product.Domain ↓Product.CoreCore saf domain modellerini içerir.
Entity Framework veya infrastructure dependency içermez.
Domain persistence katmanını, DbContext’leri ve repository yapılarını içerir.
Api ise composition root olarak davranır ve gerektiğinde farklı bounded context’leri aynı use-case içerisinde orkestre eder.
eski-dev’deki mevcut referans yapısındaki dependency yönü de Gold.Api → Gold.Domain → Gold.Core şeklindedir. Core katmanının infrastructure paketlerinden bağımsız tutulması temel kurallardan biridir.
Bugün bu mimariyi tek cümleyle anlatmam gerekirse:
ARC-DDD, klasik katmanlı DDD yaklaşımını yatay bounded context’ler ve ortak tablolar üzerinde çalışan domain-specific entity projection’larıyla genişleten bir yazılım mimarisi yaklaşımıdır.
Bunu daha teknik olarak şöyle tanımlıyorum:
Classic layered DDD \+ horizontal bounded-context folders in Core \+ per-context entity projections over shared tables.
Ama benim için bu yaklaşımın arkasındaki daha önemli fikir şu:
Bir domain, sistemde var olan bütün bilgiyi değil, yalnızca kendi işini yapmak için ihtiyaç duyduğu dünyayı görmelidir.
Çünkü yaklaşımın kökeni benim için Avustralya’ya dayanıyor.
Associate Engineer olarak çalıştığım dönemde Adrian Budiman’dan öğrendiğim yatay domain ayrımı fikri başlangıç noktasıydı.
Ben bu fikri daha sonra kendi sistemlerimde deneyerek; domain-local entity modelleri, shared-table projection’ları, domain başına DbContext ve kontrollü cross-domain composition prensipleriyle genişlettim.
Dolayısıyla ARC-DDD’yi anlatırken bunu “her şeyi sıfırdan ben icat ettim” şeklinde anlatmayı doğru bulmuyorum.
Mühendislik zaten çoğu zaman böyle ilerlemez.
Birisi size güçlü bir fikir verir.
Siz onu başka problemlere uygularsınız.
Eksiklerini görürsünüz.
Üzerine yeni prensipler eklersiniz.
Ve yıllar sonra başlangıçtaki fikirden oldukça farklı bir sistem ortaya çıkar.
ARC-DDD benim için tam olarak böyle gelişti.
Adrian’ın bana gösterdiği yatay domain düşüncesi kıvılcımdı.
Benim katkım ise o kıvılcımı bugün ARC-DDD adını verdiğim uygulanabilir bir mimari modele dönüştürmek oldu.
ARC-DDD yalnızca teorik olarak tarif ettiğim bir mimari yaklaşım değil.
Yaklaşımın gerçek bir sistem içerisinde nasıl uygulandığını görmek isteyenler için eski geliştirme kodlarımdan birini public olarak yayınladım:
GitHub:
https://github.com/Arcnaboo/eski-dev
Repository public durumdadır, C\# tabanlıdır ve MIT lisansı altında yayınlanmıştır.
Bu repository, ARC-DDD’nin public Goldtag legacy backend reference implementation’ıdır.
Repository referans implementasyonu açık şekilde:
Gold.CoreGold.DomainGold.Apiolarak tanımlıyor.
Repository içerisinde özellikle şu modellere bakmanızı öneririm:
Gold.Core/Users/User.csGold.Core/Events/User2.csGold.Core/Transactions/User3.csBunlar ARC-DDD’nin en önemli fikrini doğrudan gösteriyor.
Canonical User, Users bounded context’ine aittir.
Events aynı kullanıcının kendi ihtiyacına göre daraltılmış User2 modelini kullanır.
Transactions ise yine kendi operasyonlarına uygun User3 modelini kullanır.
Aynı yaklaşımı Event ve Event2, Notification ve Notification2 gibi modellerde de görebilirsiniz. Referans implementasyonu öğrenmek için repository’de özellikle bu dosyalara bakın.
Burada önemli bir ayrım yapmak gerekiyor.
Bu repository eski ve gerçek bir production codebase’in izlerini taşıyor.
Dolayısıyla içindeki her implementation detayını bugün sıfırdan yazacağım bir sistemde birebir kullanmam.
Örneğin eski connection string yöntemleri, bazı controller yapıları, eksik dependency injection kullanımları ve artık ihtiyaç olmayan legacy parçalar ARC-DDD’nin kendisi değildir.
Bu farkı özellikle belirtiyorum:
Fikirleri alın, mekanikleri modernize edin.
Kalıcı fikir:
Horizontal bounded contexts \+ per-domain projections \+ per-domain DbContexts.
Eski kodun tarihsel implementation detayları değil.
Bir mimari yaklaşımın sadece diyagramlarla anlatılmasının yeterli olmadığını düşünüyorum.
İnsanların:
gerekiyor.
Bu nedenle ARC-DDD için hem mimari dokümantasyonu hem de gerçek bir public reference implementation’ı erişilebilir tutmak istiyorum.
Repository:
https://github.com/Arcnaboo/eski-dev
Ama ARC-DDD’yi anlamak için repository’deki her satırı kopyalamanızı istemiyorum.
Tam tersine.
Kodun yaşından gelen parçaları ayırın.
Domain sınırlarına bakın.
Aynı fiziksel entity’nin farklı bounded context’lerde nasıl farklı modellendiğine bakın.
Context’lerin nasıl ayrıldığına bakın.
Core’un neden başka domain’lerin modellerini import etmediğine bakın.
Çünkü ARC-DDD’nin özü bir framework veya belirli bir teknoloji değildir.
Entity Framework değişebilir.
.NET değişebilir.
Database değişebilir.
Repository pattern değişebilir.
Yıllar sonra bugün kullandığımız araçların büyük bölümü de değişmiş olabilir.
Ama temel prensip değişmez:
Domain sınırlarını kodda gerçek sınırlar haline getir.
Ve her domain’e yalnızca ihtiyaç duyduğu dünyayı göster.