Domain-driven design nedir?
Domain-driven design (DDD), iş kurallarını merkeze alan, kodu işin diliyle hizalayan bir yazılım tasarım yaklaşımıdır. Teknik kararları, alan uzmanlarının tarif ettiği kurallara göre şekillendirirsiniz. Kod yapısı, veri tabanı şeması ya da framework değil, domain yönlendirir.
Domain driven design, karmaşık iş alanlarında size güçlü bir model kurma zemini sağlar. Burada domain; finans, e-ticaret, lojistik ya da insan kaynakları gibi iş alanınızdaki kavramlar bütünü anlamına gelir. Siz, entity, value object, aggregate gibi taktik desenlerle bu alanı koda taşırsınız. DDD, sadece sınıf isimlerini düzeltmekten ibaret değildir. Ekip yapısını, sınırları, konuşma biçimini de etkileyen bütünsel bir yaklaşım sunar.
Bu yaklaşımda üç temel odak bulunur. İş alanını derinlemesine anlamak, bu alanı net bir model ile temsil etmek ve bu modeli sürdürülebilir bir kod tabanına dönüştürmek. Domain driven design felsefesi, “önce veritabanı şeması” refleksine ters düşer. Önce dil, kurallar ve davranışları konuşursunuz. Ardından, bunları yansıtan bir tasarım kurarsınız. Geliştirici ekiple domain uzmanını aynı masaya oturtan şey de aslında budur.
Ben DDD’yi özellikle şu durumlarda etkili buluyorum:
- Kurallar sık değişen, karmaşık iş süreçleri olan sistemlerde
- Tek ürün yerine ürün ailesi ya da platform geliştiren ekiplerde
- Uzun ömürlü, yıllarca evrim geçirecek kurumsal uygulamalarda
Kısacası domain driven design, “önce kod” yerine “önce anlam” diyen, iş diliyle yazılımın tasarımını aynı çizgide tutmaya çalışan bir yaklaşım sunar. Mevcut bir sistemde bu sınırların nerede kırıldığını görmek, yazılım mimarisi danışmanlığı kapsamındaki incelemenin bir parçasıdır.
DDD'nin stratejik tarafları
DDD’yi gerçekten uygulamak istiyorsanız önce stratejik tarafı kavramanız gerekir. Çünkü bounded context ve yaygın dil oturmadığında, entity ve aggregate desenleri tek başına sistemi toparlamaz. Stratejik düzey, mimari sınırları ve ekip organizasyonunu şekillendirir. Teknik lider olarak attığınız her mimari adım, aslında domain haritasına referans vermelidir.
Stratejik tarafta iki ana konu öne çıkar. Birincisi, domaini mantıklı parçalara ayıran bounded context kavramı. İkincisi, ekiplerin kullandığı dili netleştiren ubiquitous language yaklaşımı. Bu ikili, hem kod yapısını hem de ekipler arası iletişimi düzenler. Monolit ya da mikroservis fark etmez, context sınırlarını tanımlamadan sağlıklı ölçeklenme beklentisi gerçekçi olmaz.
Örneğin bir sipariş sistemi düşünelim. Siparişi yöneten context ile faturalamayı yöneten context aynı entity set’ini paylaşmamalıdır. Sipariş tarafı Order ve OrderItem ile ilgilenirken, muhasebe tarafı farklı bir model kullanabilir. Burada amaç, tek bir “mükemmel” model kurmak değildir. Her bounded context, kendi iş yüküne uygun bir model taşır. Bu ayrımı netleştirmeyen ekipler, zamanla devasa ve kırılgan bir ortak model içinde kaybolur.
Stratejik DDD, sadece kodu değil organizasyon yapısını da etkiler. Domain driven yaklaşım, ekipleri domain etrafında hizalamayı savunur. Böylece her ekip, kendi context’inin sahibi olur ve sorumluluk sınırları netleşir.
Sınırlı bağlam (bounded context)
Bounded context, belirli bir domain modelinin geçerli olduğu sınırdır. Yani aynı “müşteri” kavramı, satış ve destek context’lerinde farklı anlamlar taşıyabilir. Siz, bu farkı teknik olarak da görünür kılarsınız. Her context kendi modelini, verisini ve dilini taşır. Bu yaklaşım, model erozyonunu engeller. Ayrıca ekipler arasındaki bağımlılıkları azaltır.
Yaygın dil (ubiquitous language)
Ubiquitous language, ekip içindeki herkesin aynı kavramları aynı anlamda kullanmasıdır. Analist, geliştirici ve domain uzmanı “OrderItem” dediğinde aynı şeyi kasteder. Bu dil, hem kodda hem dokümantasyonda görünür olmalıdır. Sınıf isimleri, metot adları ve hatta public string alanlar bu dili yansıtmalıdır. Böylece iletişim hataları azalır ve model daha tutarlı gelişir.
DDD'nin taktiksel tarafları
Stratejik çerçeve oturduktan sonra, işin taktik kısmına inersiniz. Burada entity, value object, aggregate, domain service, repository gibi desenler devreye girer. En çok konuşulan kısım da aslında burasıdır. Ancak taktik desenler tek başına DDD sayılmaz. Yine de doğru kullanıldığında, domain driven design yaklaşımını somut koda dönüştürmenize yardımcı olur.
Taktik seviyede temel amaç, domain modelini davranış odaklı şekilde ifade etmektir. Sadece veri taşıyan sınıflar yerine, iş kurallarını üzerinde barındıran zengin model tercih edilir. Entity, kimlik üzerinden takip ettiğiniz kavramları temsil eder. Value object ise kimlikten bağımsız, değeriyle tanımlanan yapılardır. Aggregate, bu ikisini anlamlı bir bütün içinde toparlar. Bir aggregate root, dış dünyaya açılan tek giriş kapısı gibi davranır.
Mesela bir sipariş domain’inde Order entity, satırları ise value object olabilir. Order aggregate, toplam tutar hesabı ya da quantity kontrolü gibi kuralları kendi içinde tutar. Dış katmanlar, aggregate içindeki nesnelere doğrudan müdahale etmez. Bu yaklaşım, invariants dediğimiz değişmez kuralları korumanıza yardım eder. Ben, “her yerden her şeye erişim” mantığının uzun vadede sistemi zayıflattığını düşünüyorum.
Taktik DDD, ORM aracı ya da framework seçiminden bağımsızdır. Siz entity ve value object tasarımını netleştirdikten sonra, persistence katmanını buna uydurursunuz. Tersini yaptığınızda, yani veritabanı şemasını merkeze aldığınızda, domain driven tasarım etkisini büyük ölçüde kaybeder.
Bu seviyede dikkat etmeniz gereken temel noktaları adım adım özetleyebilirim:
- Önce domain modelini, sonra tablolara yansıyacak yapıyı düşünün.
- Entity ve value object ayrımını her kritik kavram için netleştirin.
- Aggregate sınırlarını, iş kurallarının birlikte değiştiği yerlere göre çizin.
- Public metotları, domain dilini yansıtacak şekilde adlandırın.
Varlık (entity)
Entity, kimliğiyle tanımlanan domain nesnesidir. “Müşteri”, “Sipariş” ya da “Kullanıcı” gibi kavramlar genellikle entity olur. Kimlik değişmediği sürece, diğer alanlar değişse de aynı entity’den bahsedersiniz. Entity içinde davranış barındırmak önemlidir. Sadece property set eden sınıflar, domain driven yaklaşımı zayıflatır. Kuralları entity metotlarına taşıyarak, iş mantığını merkezde tutarsınız.
Değer nesnesi (value object)
Value object, kimlik yerine değeriyle tanımlanan yapılardır. Para, adres ya da tarih aralığı gibi kavramlar buna örnek olur. İki value object aynı alan değerlerine sahipse eşittir. Genellikle immutable tasarlarsınız. Yani oluşturduktan sonra değiştirmezsiniz, yenisini üretirsiniz. Bu yaklaşım, hata riskini azaltır. Ayrıca domain kurallarını bu küçük nesnelerin içine gömmenize izin verir.
Agregat (aggregate)
Aggregate, bir grup entity ve value object’i mantıklı bir bütün olarak bir araya getirir. Dış dünyaya sadece aggregate root açılır. Root, invariants dediğimiz değişmez kuralları korur. Örneğin, bir sipariş aggregate’i toplam miktar ve durum geçişlerini kontrol eder. Diğer nesneler, root üzerinden yönetilir. Bu yapı, transaction sınırlarını da netleştirir. Bu sayede, tutarlı ve daha dayanıklı bir model kurarsınız.
Domain events
Domain events, domain içinde olan anlamlı değişiklikleri temsil eder. “Sipariş onaylandı”, “Ödeme başarısız oldu” gibi olaylar buna örnektir. Siz bu olayları, sistemin geri kalanına açık sinyaller gibi düşünebilirsiniz. Domain event kullanımı, modüller arası gevşek bağlılık sağlar. Bir context, diğerini doğrudan çağırmak yerine event üzerinden tepki verebilir.
Domain events nesnelerini genellikle küçük, okunabilir sınıflar halinde tasarlarsınız. Olayın ne zaman gerçekleştiği, hangi entity ile ilgili olduğu ve gerekli minimal veri bu nesnede yer alır. Bu nesneler, persistence detaylarından bağımsız kalmalıdır. Amaç, iş alanında önemli olan değişimi ifade etmektir. Teknik altyapı, bu olayı async kuyruklara ya da log sistemlerine iletebilir.
Domain event yaklaşımı, özellikle entegrasyon senaryolarında fayda sağlar. Söz gelimi, bir context içinde sipariş tamamlandığında event fırlatırsınız. Faturalama context’i bu olayı dinler ve kendi modeline göre tepki verir. Bu sayede, context’ler birbirinin internal detayına bağımlı hale gelmez. Doğrudan servis çağrılarıyla örülen entegrasyonlar zamanla kırılganlaşır.
Domain events ile teknik event’leri karıştırmamak da önemlidir. Loglama ya da altyapı sinyalleri ayrı bir katmanda kalmalıdır. Domain event, iş uzmanının da anlayacağı bir dilde olmalıdır. İsimlendirme burada kritik rol oynar. “OrderApproved” ya da “PaymentFailed” gibi net isimler, ekip içi iletişimi güçlendirir.
DDD ne zaman değer katar?
Her projeye DDD uygulamak zorunda değilsiniz. Hatta küçük ve basit iş akışlarında bu yaklaşımı ayrıntılı uygulamak zaman kaybı yaratabilir. Benim net görüşüm şu: DDD, karmaşıklığın gerçekten hissedildiği alanlarda parlamaya başlar. Basit CRUD uygulamalarında, bu yaklaşım çoğu zaman gereksiz ağırlık oluşturur.
DDD’nin değer kattığı tipik durumları üç başlıkta toplayabilirim:
- İş kuralları sık değişiyor ve ekip bunları takip etmekte zorlanıyorsa
- Birden fazla sistem, aynı domain etrafında entegre çalışıyorsa
- Farklı ekipler aynı alan üzerinde paralel geliştirme yapıyorsa
Bu sitenin de üzerinde çalıştığı içerik yönetim sistemimiz YouCMS'e eklenti altyapısı kurarken DDD'nin ağır araçlarını değil, yalnız sınır fikrini aldım. Sınırları modül olarak çizdik. Her eklenti çekirdekle yalnız ortak bir sözleşme paketi üzerinden konuşuyor; o paket çekirdeğin hiçbir katmanına bağımlı değil ve eklentilerin altyapı katmanına referans vermesi yasak. Buna karşılık her modüle ayrı veritabanı bağlamı vermedik, tek bağlamı ve tek migration hattını koruduk. Modüller arası mesajlaşma için de hazır bir kütüphane yerine 150 satır kadar bir olay yolu yazdık. Kazancımız: eklenti altyapısı mevcut davranışı değiştirmeden geldi, sistemin çıktısı eskisiyle birebir aynı kaldı ve test sayısı 161'den 201'e çıktı.
Diyelim ki, karmaşık fiyatlandırma kuralları olan bir abonelik sistemi düşünün. Kampanyalar, istisnalar, segment bazlı kurallar sürekli değişiyor. DDD ile bu kuralları domain modeline taşıdığınızda, değişiklikleri yönetmek daha öngörülebilir hale gelir. Aksi durumda, dağınık if blokları ve dairesel bağımlılıklar içinde kaybolursunuz.
Öte yandan, sadece birkaç form ve rapordan oluşan bir iç uygulama için ağır bir domain katmanı kurmak mantıklı olmayabilir. Basit bir veri odaklı tasarım, bakım maliyetini azaltır. Burada kritik soru şudur: Sistem, önümüzdeki yıllarda ne kadar evrim geçirecek ve karmaşıklık hangi hızda artacak? Eğer uzun vadede ciddi bir büyüme beklemiyorsanız, DDD’yi hafif dokunuşlarla sınırlı tutabilirsiniz.
Bu noktada, DDD ile daha basit katmanlı mimarileri yan yana görmek faydalı olur.
| Yaklaşım | Avantaj | Dezavantaj |
|---|---|---|
| DDD odaklı model | Karmaşıklık yönetimi | Öğrenme eğrisi |
| Basit katmanlı yapı | Hızlı başlangıç | Zayıf model |
| Veritabanı odaklı | Kolay raporlama | Sıkı bağımlılık |
Bu tabloya bakınca, domain driven design yaklaşımını özellikle uzun soluklu ve değişken iş alanlarında tercih etmenin daha rasyonel olduğunu söyleyebilirim.
Sık sorulan sorular
DDD için mikroservis şart mı?
Hayır, DDD için mikroservis zorunlu değildir. Monolit bir mimari içinde de bounded context sınırlarını kod seviyesinde çizebilirsiniz. Mikroservis, bu sınırları dağıtık hale getirir ama DDD’nin ön koşulu sayılmaz.
Bounded context nasıl belirlenir?
Bounded context sınırlarını iş dilindeki anlam değişimlerine bakarak belirlersiniz. Farklı ekipler aynı terimi farklı anlamda kullanıyorsa, genellikle ayrı context ihtiyacı vardır. Bunun yanı sıra, veri sahipliği ve değişim ritmi de sınırları netleştirmeye yardım eder.
Aggregate ile entity arasındaki fark nedir?
Entity, kimliği olan tekil domain nesnesidir; aggregate ise bu nesneleri mantıklı bir bütün içinde toplayan yapıdır. Aggregate root, dış dünyaya açılan tek giriş noktasıdır. Diğer entity ve value object’lere erişimi bu root üzerinden yönetirsiniz.
DDD küçük projelerde kullanılır mı?
Evet, ancak genellikle hafifletilmiş bir biçimde kullanmak daha uygundur. Küçük projede temel kavramları alıp, aşırı soyutlamadan uygulayabilirsiniz. Tam kapsamlı stratejik DDD, küçük ve kısa ömürlü projeler için çoğu zaman gereksiz ağırlık yaratır.
Domain Driven Design uygularken nelere dikkat edilmeli?
Öncelikle domain uzmanlarını sürecin merkezine almanız gerekir. Ardından bounded context sınırlarını netleştirip, ubiquitous language’i kodda görünür kılmalısınız. Son olarak, entity ve aggregate tasarımını persistence detaylarından bağımsız düşünmeye özellikle dikkat etmelisiniz.
Katman ve bağımlılık sınırlarını DDD ile birlikte düşünmek için clean architecture nedir yazısına, mimari türlerinin genel resmi için yazılım mimarisi nedir yazısına; şirket ölçeğindeki teknoloji kararları için yazılım danışmanlığı sayfasına bakabilirsiniz.
