Domain-Driven Design, yazılım geliştirme sürecini sadece bir kodlama faaliyeti olarak değil, aynı zamanda insan ilişkileri, mimari stratejiler ve iş prensiplerinin birleşimi olarak ele alan Domain-Centric bir yaklaşımdır. DDD’nin temel felsefesi, yazılımın kalbinde yer alan karmaşık iş problemlerini efektif bir şekilde ele almak, business team (buna iş analisti, ürün geliştiricileri gibi) ile development team (teknik ekip) arasında iyi bir iletişim geliştirmektir. Bu yaklaşım bize projenin ana hedefinin teknolojik araçlardan ziyade çözülmesi gereken bir iş olması gerektiğini savunur. Bu konuyu örnekleyerek daha iyi aktaracağım.
DDD pratikleri uygulanmayan projelerde en büyük engel olarak karşımıza kötü iletişim çıkar. Teknik ekibin teknoloji odaklı bir dil konuşması, iş biriminin ise tamamen süreçsel bir dil benimsemesiyle birlikte gereksinimlerin koda dökülme aşamasında ciddi sapmalara neden olur. Bu durum projenin bir noktadan sonra teknoloji tabanlı problem yumağına dönüşmesine yol açar.
Geleneksel yaklaşımlarda kod içerisinde sıkça karşılaşılan kimliksiz nesneler yani genel geçer isimlerle oluşturulmuş “Manager“, “Helper” veya “GlobalService” gibi sınıflar, iş mantığının nereye ait olduğunu belirsizleştirir. Bu durum bizi Object-Oriented Programming prensiplerinden uzaklaştırır.
En yaygın teknik sorunlardan biri olan Anemic Domain Model nesnelerin sadece veriyi tutan ancak hiçbir davranış sergilemeyen boş yapılar olmasıdır. Bu modeller iş kurallarını kendi üzerlerinde taşımadıkları için sistemin bakımı ve genişletilmesi zamanla imkansız hale gelir. Entity nesneler içerisinde hiçbir metot bulunmaması, sadece { get; set; } özelliklerinden oluşması OOP prensiplerine aykırıdır. Çünkü nesne, veriyi ve o veri üzerindeki davranışı bir arada tutmalıdır.
Martin Fowler bu problemi 2003 yılında kaleme almıştır. Anemic Domain Model: https://www.martinfowler.com/bliki/AnemicDomainModel.html
DDD sunduğu prensiplerle bu kaosu yönetilebilir bir yapıya kavuşturmayı vaat eder. İlk olarak her ekibin arasında ortak bir dil kurarak iletişim verimliliğini sağlar. Böylece iş analistinin “reseller” dediği şey kod tarafında da aynı isimle ve aynı kurallarla geliştirilir. Yazılımın odağını teknolojik araçlardan (database, cache gibi) alıp çekirdek bir mimariyi oluşturur.
DDD Seçimi
DDD kullanımı bir mühendislik tercihi değil bir iş yatırım kararıdır. Bu mimariyi kullanıp kullanmama kararı iki ana kriterin kıyaslanmasıyla belirlenebilir. Birincisi projenizde rekabetçi avantaj sağlar mı sorusunu yanıtlamalısınız. Örneğin bir e-ticaret firması için kampanya motoru rekabetçi avantajdır ama personel izin sistemi sadece destekleyici bir birimdir.
İkincisi ise daha değerli bir sorudur: Bu mimariyi ekiplerin sürekli toplantı yapmasını ve kodlama süresinin uzamasını gerektirir. Bu maliyete değecek bir karmaşıklık var mı?
İyi hatırlıyorum, hukuki orta ölçekli bir projenin karmaşık entity repository yapısını DDD’ye çevirmek tek başıma 2 haftamı almıştı. Bu biraz abonelerin yenilikleri almasını geciktirdi fakat yeni geliştiriciler projeyi açtıklarında her domain ayrılmış, her entity nesnesinin kendi içinde metotları ve temiz bir namespace yapısı vardı.
Elbette iş mantığı basitse (sadece CRUD işlemlerinin olduğu projeler gibi) bu yapıyı kullanmak sinek öldürmek için top atmaya benzer ki bu gereksiz maliyettir.
Ubiquitous Language
Bu terim iletişimin dilidir. İş birimi ile teknik ekibin birbirini anlamak için aynı terimlerle konuştuğu dil. Bu tanımı biraz somutlaştıralım. Örneğin iş birimi “Üyelik Askıya Alındı” diyorsa, yazılımcı kendi kafasında bunu IsActive = false veya Status = 2 olarak çevirmemelidir. Kodda tam olarak SuspendMembership() yazmalıdır.
public class Membership
{
public Guid Id { get; private set; }
public string MemberName { get; private set; }
public MembershipStatus Status { get; private set; }
public string SuspensionReason { get; private set; }
public DateTime? SuspendedAt { get; private set; }
public Membership(string name)
{
Id = Guid.NewGuid();
MemberName = name;
Status = MembershipStatus.Active;
}
public void SuspendMembership(string reason)
{
if (Status == MembershipStatus.Suspended)
{
throw new InvalidOperationException("Member already suspended");
}
if (string.IsNullOrWhiteSpace(reason))
{
throw new ArgumentException("Üyeliği askıya almak için bir sebep belirtilmelidir.");
}
this.Status = MembershipStatus.Suspended;
this.SuspensionReason = reason;
this.SuspendedAt = DateTime.UtcNow;
// Event publish
}
}
public enum MembershipStatus
{
Active,
Suspended,
Cancelled
}İş birimi bir kelimeyi (örneğin sipariş) kullandığında teknik ekibin aklına veritabanındaki orders tablosu gelmemeli. O siparişin hangi kurallarla iptal edilebileceği ve hangi aşamalardan geçtiği gelmelidir.
Bounded Context
Bounded Context kavramını anlamak için microservice ile olan “yumurta-tavuk” ilişkisini çözmek gerekir. Bounded Context sadece microservice mimarilerinde kullanılmaz ancak iyi bir microservice mimarisi için Bounded Context şarttır.
Eğer bu yapıyı kullanmazsanız projeniz büyüdükçe Big Ball of Mud (büyük çamur yığını) oluşur.
Büyük ve karmaşık bir yazılımı tek bir devasa blok olarak yönetemezsiniz. Bu yüzden onu mantıksal ve teknik sınırlarla birbirinden kopuk küçük parçalara ayırmalısınız. Eskiden biz her şeyi tek bir veritabanına, tek bir devasa core projesine koyardık. Bu projeler büyüdükçe içinden çıkılmaz hale gelirdi.
Örneğin faturalama ile üyelik aynı şey olmadığı için bunları birbirinden ayırmalısınız. Her biri kendi veritabanına, kendi modellerine ve kendi kurallarına olmalı. Eğer haberleşmeleri gerekiyorsa birbirlerinin içine girmeden, belirli kapılar (API, Event vb.) üzerinden haberleşmeliler.

Görsel: ByteByteGo
Microservice bir dağıtım (deployment) stratejisidir, Bounded Context ise mantıksal bir sınırdır. Bir e-ticaret sisteminde müşteri satış ekibi için para ödeyen kişi, destek ekibi için sorun bildiren kişidir. Doğal olarak tek bir “customer” sınıfı yaparsanız 100 property olan devasa bir sınıfa dönüşür. Bounded Context ile her ekip kendi “customer” modelini yönetir.
DDD aslında microservice dünyasından önce çıktı. Büyük bir monolith projede klasör yapıları ve modüller (Module A, Module B) ile Bounded Context oluşturabilirsiniz. Microservice mimarisinde Bounded Context, servislerin sınırlarını belirleyen çerçevedir Genellikle her bir Bounded Context bir microservice olarak ayağa kaldırılır. Ancak tersi her zaman doğru değildir.
Aggreagate ve Aggreagete Root
Domain-Driven Design (DDD) mimarisinde belirli bir iş kuralını bir araya gelen ilişkili nesneler bütünü Aggregate (küme) olarak tanımlanır. Bu kümenin yönetim merkezi ise Aggregate Root (küme kökü) nesnesidir. Bir e-ticaret sistemindeki Order, OrderProduct, OrderInvoice gibi entity sınıflar birbirlerine çok bağlı yapılardır. Bütünsel bir akışı tamamladıkları için bir aggregate oluştururlar. Burada “Order” aggregate root olur.

Görsel: Microsoft
Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An example may be an order and its line-items, these will be separate objects, but it’s useful to treat the order (together with its line items) as a single aggregate (Martin Fowler)
“Order” aggregate için bir kod örneği oluşturalım.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Domain.Orders
{
public interface IAggregateRoot { }
// Bu sınıf tüm işlemleri yönetir. Yani "Aggregate Root"
public class Order : IAggregateRoot
{
public Guid Id { get; private set; }
public Guid BuyerId { get; private set; }
public DateTime OrderDate { get; private set; }
public OrderStatus Status { get; private set; }
public Address ShippingAddress { get; private set; }
// Encapsulation
private readonly List<OrderItem> _orderItems = new();
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
// ORM için (EF, Dapper gibi) boş constructor gerekli
private Order() { }
public Order(Guid buyerId, Address shippingAddress)
{
Id = Guid.NewGuid();
BuyerId = buyerId;
ShippingAddress = shippingAddress;
OrderDate = DateTime.UtcNow;
Status = OrderStatus.Submitted;
}
// Ürün ekleme sadece bu metotla olabilir
public void AddOrderItem(Guid productId, string productName, decimal unitPrice, int quantity)
{
if (quantity <= 0) throw new Exception("Miktar sıfırdan büyük olmalıdır.");
var existingItem = _orderItems.SingleOrDefault(o => o.ProductId == productId);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
var newItem = new OrderItem(productId, productName, unitPrice, quantity);
_orderItems.Add(newItem);
}
}
public decimal GetTotalAmount() => _orderItems.Sum(i => i.UnitPrice * i.Quantity);
}
// 3. Entity object. Varlığı sadece Aggregate Root içinde anlamlı
public class OrderItem
{
public Guid Id { get; private set; }
public Guid ProductId { get; private set; }
public string ProductName { get; private set; }
public decimal UnitPrice { get; private set; }
public int Quantity { get; private set; }
internal OrderItem(Guid productId, string productName, decimal unitPrice, int quantity)
{
Id = Guid.NewGuid();
ProductId = productId;
ProductName = productName;
UnitPrice = unitPrice;
Quantity = quantity;
}
internal void IncreaseQuantity(int quantity) => Quantity += quantity;
}
// Value object. Sadece değerleri ile tanımlanan nesne.
public record Address(string Street, string City, string Country);
public enum OrderStatus { Submitted, Paid, Shipped, Cancelled }
}Entity Object ve Value Object
Bir nesneyi diğerlerinden ayıran ömrü boyunca değişmeyen bi ID değeri varsa Entity olarak adlandırılır. Yani içindeki veriler (isim, miktar, fiyat) değişse bile nesne aynı nesnedir. Siz “Product” içinde isim ve fiyat güncelleseniz bile ID aynı kalacağı için ürün kimliği değişmez.
Value object ise kimliksiz nesnelerdir. Genellikle değişmezler (Immutable bir object). Bir adresin sokağı değiştiğinde o artık farklı bir adrestir. Ülkeler, döviz birimleri gibi çok örnek verilebilir.
Küçük hatırlatma: Aggregate bir nesne değil bir kümedir. Bir veya birden fazla entity veya value object bir araya gelerek oluşturulan küme.
Domain Service
Manager sınıfları DDD literatüründeki domain service sınıfıdır. Birçok kurumsal mimaride (özellikle ABP Framework gibi yapılarda) “Manager” ismi tercih edilir. Manager içinde repository kullanmak daha rahat olabilir ancak bu business logic ile ilişkilidir. Dengeyi iyi sağlamalısınız, basit bir ekleme çıkarma gibi işlemler için kullanılmaz.
Karmaşık bir iş kuralı varsa ve veritabanından veri çekme işlemi birçok koşula göre değişebiliyorsa manager veya domain service kullanabilirsiniz. Sadece _manager.DoWork() dersiniz ve o arka planda her şeyi halleder.
Basit bir örnek:
// Domain Service (Manager)
public class OrderManager
{
private readonly IOrderRepository _orderRepository;
public OrderManager(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task ApplyLoyaltyDiscountAsync(Order order)
{
// Manager gidip veritabanından geçmişi sorguluyor
var pastOrdersCount = await _orderRepository.GetTotalOrderCountAsync(order.BuyerId);
if (pastOrdersCount > 10)
{
order.ApplyDiscount(0.15m); // %15 indirim
}
}
}Application Service
Dış dünyadan (API/UI) gelen isteği karşılayan, gerekli verileri domain repository sınıflarından çeken ve domain nesnelerini konuşturan katmandır. Orkestra şefidir. DB kaydı (Unit of Work), log, cache ve mapping burada yapılır. Domain service, API, repository gibi çok şeyi kullanabilir.
Order Application Service oluşturarak konuyu daha iyi anlayabiliriz.
public class OrderAppService : IOrderAppService
{
private readonly IOrderRepository _orderRepository;
private readonly OrderManager _orderManager;
private readonly IUnitOfWork _uow;
public OrderAppService(IOrderRepository orderRepo, OrderManager orderManager, IUnitOfWork uow)
{
_orderRepository = orderRepo;
_orderManager = orderManager;
_uow = uow;
}
public async Task<OrderDto> CreateOrderAsync(CreateOrderRequest request)
{
var buyer = await _orderRepository.GetBuyerAsync(request.BuyerId);
var order = _orderManager.CreateOrderWithDiscount(buyer, request.Items);
await _orderRepository.AddAsync(order);
await _uow.SaveChangesAsync();
return new OrderDto { Id = order.Id, TotalPrice = order.TotalPrice };
}
}Sonuç
Yazılım mimarisi sadece basit kod bloklarını alt alta yazmak değildir. En önemli şey iş mantığını iyi kurgulamaktır. Martin Fowler ve Eric Evans gibi geliştiricilerin savunduğu DDD yaklaşımı, bizi daha akıllıca proje geliştirmeye davet ederek kodu teknik bir zorunluluktan çıkarıp iş biriminin dünyasıyla ortak bir dile (ubiquitous language) dönüştürür. Bu felsefede Aggregate Root bir otorite figürü olarak veri bütünlüğünü savunur, Domain Service ise saf mantığın elçisidir. Böylece temiz bir namespace yapısı içinde ne istediğini bilen, sürdürülebilir bir sistem inşa edilir. Sonuçta mesele sadece fonksiyon yazmak değil, iş kurallarını mimarinin merkezine koyarak yazılımın yaşayan bir organizma gibi doğru ve tutarlı büyümesini sağlamaktır.



Yorum bırakın