← Ana sayfaya dön
14 dk okumav1.0

ARC-DDD: Avustralya’da Başlayan Bir Domain-Driven Design Yaklaşımı

Arda Akgür

DDDARC-DDDARC DDDAustralian DDDDomain-Driven DesignBounded ContextSoftware Architecture

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 DDD’de Problem Ne?

Klasik Domain-Driven Design genellikle sistemi dikey katmanlara ayırır:

  • Presentation
  • Application
  • Domain
  • Infrastructure

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:

Plain Text
Core├── Users├── Events├── Transactions├── Vendors└── Payments

Buradaki 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.

Bir User Her Domain İçin Aynı User Değildir

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:

Plain Text
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:

Plain Text
Users.UserEvents.User2Transactions.User3

Bunlar farklı domain modelleridir.

Ama aynı fiziksel database tablosunu temsil edebilirler.

Plain Text
Users.User          ──┐Events.User2        ──┼──> Users tableTransactions.User3 ──┘

Events domain’i yalnızca şunlara ihtiyaç duyuyorsa:

Plain Text
UserIdFirstNameFamilyNamePhoto

User2 sadece bunları içerir.

Transactions domain’i:

Plain Text
UserIdBalanceIBANVerified

alanları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.

Aynı Database, Farklı Domain Gerçeklikleri

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:

C#
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.

Her Domain Kendi Persistence Dünyasına Sahiptir

Bu ayrım yalnızca klasör seviyesinde kalmaz.

Her bounded context kendi DbContext yapısına sahip olur.

Örneğin:

Plain Text
UserDbContextEventsDbContextTransactionDbContextVendorsDbContext

Events 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:

C#
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.

Domain’ler Nerede Birleşiyor?

Gerçek uygulamalarda bounded context’ler tamamen izole yaşayamaz.

Bir işlem aynı anda:

  • Ordering
  • Payments
  • Users

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:

Plain Text
API | +--> OrderingRepository | +--> PaymentsRepository | +--> UsersRepository

Yani 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.

Üç Temel Proje

Bugün yeni bir ARC-DDD sistemi oluşturacak olsam temel yapıyı oldukça basit tutarım:

Plain Text
Product.CoreProduct.DomainProduct.Api

Bağımlılık yönü:

Plain Text
Product.ApiProduct.DomainProduct.Core

Core 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.

ARC-DDD’nin Tek Cümlelik Tanımı

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.

Neden “Australian DDD”?

Çü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.

Public Reference Implementation

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:

Plain Text
Gold.CoreGold.DomainGold.Api

olarak tanımlıyor.

Repository içerisinde özellikle şu modellere bakmanızı öneririm:

Plain Text
Gold.Core/Users/User.csGold.Core/Events/User2.csGold.Core/Transactions/User3.cs

Bunlar 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.

Neden Public Olarak Paylaşıyorum?

Bir mimari yaklaşımın sadece diyagramlarla anlatılmasının yeterli olmadığını düşünüyorum.

İnsanların:

  • gerçek kodu açabilmesi,
  • entity’leri karşılaştırabilmesi,
  • DbContext mapping’lerini görebilmesi,
  • repository sınırlarını inceleyebilmesi,
  • yaklaşımı eleştirebilmesi

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.