🧠 Mikroservislerde Rich Domain Modelleri: DDD ve CQRS ile Karmaşayı Ehlileştirin

Mikroservis mimarileri, günümüz yazılım dünyasının parlayan yıldızı. Esneklik, ölçeklenebilirlik ve bağımsız geliştirme imkanı sunarken, karmaşık iş mantığını yönetmek ve sürdürülebilir bir kod tabanı oluşturmak çoğu zaman büyük bir meydan okuma haline gelebilir. Her şeyin sadece veri tabanına yazıp okumaktan ibaret olduğu "Anemic Model" yaklaşımlarıyla baş etmek, uzun vadede sürdürülemez bir kod tabanına ve teknik borca yol açabilir.

Peki, bu karmaşayı nasıl dizginleyebiliriz? Cevap: Domain-Driven Design (DDD) prensiplerini uygulayarak ve mikroservislerinizi Rich Domain Model ile donatarak. Gelin, bu yolculuğa birlikte çıkalım!


🧱 Rich Domain Model Nedir ve Neden Gereklidir?

Basitçe ifade etmek gerekirse, Rich Domain Model, sadece veri tutan nesnelerden ibaret değildir; aynı zamanda bu veriler üzerindeki iş kurallarını ve davranışları da içinde barındırır. Her Bounded Context (Sınırlı Bağlam) veya mikroservis için tek ve bütüncül bir Domain Model tanımlamak esastır.

Diyelim ki bir e-ticaret sistemindeki Order (Sipariş) nesnesinden bahsediyoruz. Anemik bir modelde Order, yalnızca OrderId, CustomerId, TotalAmount gibi alanlara sahip basit bir veri yapısı olurdu. Siparişle ilgili tüm iş kuralları (ürün ekleme, indirim hesaplama, stok kontrolü vb.) ise genellikle ayrı Service Layer (Servis Katmanı) içinde (örneğin bir OrderService içinde) bulunurdu.

Rich bir modelde ise Order sınıfı, AddItem(Product product, int quantity) veya ApplyDiscount(DiscountCode code) gibi metotlara sahip olurdu. Bu metotlar, siparişle ilgili tüm iş mantığını ve kurallarını (örneğin, quantity sıfırdan küçük olamaz gibi) kendi içinde barındırırdı.

Rich Domain Model vs. Anemic Model: Temel Farklar

Bu ayrım, yazılımın karmaşıklığı arttıkça hayati hale gelir:

ÖzellikRich Domain ModelAnemic Model
Data + Behavior (Veri + Davranış)Bir arada, Entity içindeAyrı; davranışlar Service Layer'da
OOP Principles (OOP Prensipleri)✅ Kapsülleme ve sorumluluklar net❌ Daha çok prosedürel yaklaşım
Application Complexity (Uygulama Karmaşıklığı)Yüksek, ancak uzun vadede sürdürülebilirDüşük, basit CRUD için yeterli olabilir
Recommended Scenario (Tavsiye Edilen Senaryo)Karmaşık Domain'lerde uzun vadeli bakım içinBasit servislerde (örneğin sadece CRUD işlemleri)

Rich Domain Model'ler, iş kurallarının kod içinde açıkça ifade edilmesini, test edilebilirliğin artmasını ve sistemin daha kolay sürdürülebilir olmasını sağlar.


🧩 DDD'nin Temel Yapı Taşları: Value Object, Entity ve Aggregate

Rich Domain Model'i inşa ederken, Domain-Driven Design (DDD)'ın temel yapı taşlarını doğru anlamak çok önemlidir.

Value Object (Değer Nesnesi) & Entity (Varlık) Ayrımı

  • Value Object: Kimliği olmayan, sadece anlam taşıyan nesnelerdir. İki Value Object, içerikleri aynıysa birbirine eşittir. Örneğin, bir Address (Adres) (sokak, şehir, posta kodu), bir Money (Para) (miktar, para birimi) veya EmailAddress (E-posta Adresi) bir Value Object olabilir. "Kim" olduğundan ziyade "ne olduğu" önemlidir.

      public record Address(string Street, string City, string ZipCode);
    

    Burada iki Address nesnesi aynı Street, City ve ZipCode değerlerine sahipse, onlar eşittir.

  • Entity: Kimliği olan, zamanla değişebilen ve yaşam döngüsü boyunca bir benzersizliğe sahip olan nesnelerdir. Bir User (Kullanıcı), Order (Sipariş) veya Product (Ürün) bir Entity'dir. Entity'nin kimliği (genellikle bir ID alanı), değerleri değişse bile sabit kalır. "Ne olduğu"ndan ziyade "kim olduğu" önemlidir.

      public class User {
          public Guid Id { get; set; }
          public string FullName { get; set; }
          public string Email { get; set; }
      }
    

🏘️ Aggregate (Topluluk) ve Aggregate Root (Topluluk Kökü) Kavramı

İşte kafa karıştırıcı olabilecek ama en kritik DDD kavramlarından biri!

  • Aggregate: Birbirine sıkı sıkıya bağlı Entity ve Value Object'lerin mantıksal bir kümesidir. Bu küme, Domain içindeki bir Consistency Boundary (Tutarlılık Sınırı)'nı temsil eder. Örneğin, bir Order (Sipariş), içindeki OrderItem'lar (Sipariş Kalemleri) ile birlikte bir Aggregate oluşturur.

  • Aggregate Root: Bu Aggregate kümesinin dış dünyayla tek erişim noktası, yani "kapı bekçisidir." Aggregate içindeki diğer nesneler (Child Entity'ler veya Value Object'ler) doğrudan dışarıdan manipüle edilemez; tüm işlemler Aggregate Root üzerinden yapılır.

    Amaç: Tutarlılığı korumak ve Domain Boundary (Alan Sınırı)'nı belirgin kılmaktır. Bir işlem sırasında, Aggregate içindeki tüm nesnelerin birlikte tutarlı kalması sağlanır. Bu, aynı zamanda Transaction Boundary (İşlem Sınırı)'nı da tanımlar.

Örnek: Order Aggregate ve Aggregate Root (.NET)

Bir siparişi yöneten Aggregate'i düşünelim:

public interface IAggregateRoot {}

// Aggregate Root
public class Order : Entity, IAggregateRoot // IAggregateRoot bir işaretleyici arayüz olabilir
{
    private List<OrderItem> _orderItems = new();
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly(); // Dışarıdan sadece okunabilir

    public DateTime OrderDate { get; private set; }
    public Address ShippingAddress { get; private set; } // Value Object

    public Order(Address shippingAddress)
    {
        ShippingAddress = shippingAddress;
        OrderDate = DateTime.UtcNow;
    }

    // OrderItem'lar sadece Order üzerinden yönetilir
    public void AddItem(int productId, string name, decimal price, int quantity)
    {
        if (quantity <= 0) 
            throw new ArgumentException("Quantity must be positive.");

        var existingItem = _orderItems.FirstOrDefault(i => i.ProductId == productId);
        if (existingItem is not null)
            existingItem.IncreaseQuantity(quantity); // İş kuralı OrderItem içinde
        else
            _orderItems.Add(new OrderItem(productId, name, price, quantity));
    }
    // Diğer iş metotları (örneğin CompleteOrder, CancelOrder vb.)
}

// Child Entity (Aggregate içindeki)
public class OrderItem : Entity
{
    public int ProductId { get; private set; }
    public string ProductName { get; private set; }
    public decimal UnitPrice { get; private set; }
    public int Quantity { get; private set; }

    public OrderItem(int productId, string productName, decimal price, int quantity)
    {
        ProductId = productId;
        ProductName = productName;
        UnitPrice = price;
        Quantity = quantity;
    }

    public void IncreaseQuantity(int amount)
    {
        // Kendi iş kuralı
        if (amount <= 0) throw new ArgumentException("Amount must be positive.");
        Quantity += amount;
    }
}

Yukarıdaki örnekte:

  • Order sınıfı Aggregate Root'tur.

  • OrderItem'lar, Order Aggregate'i içindeki Child Entity'lerdir.

  • OrderItem'lara doğrudan dışarıdan ulaşıp ekleme/silme yapılmaz; Order'ın AddItem metodu aracılığıyla yönetilirler. Bu, kuralların (örneğin Quantity artışı) tek bir yerde kontrol edilmesini ve consistency (tutarlılığın) sağlanmasını garantiler.


🌍 Bounded Context (Sınırlı Bağlam): Mahallelerin Ayrımı

Bounded Context, DDD'nin belki de en önemli kavramlarından biridir ve mikroservis mimarisinde doğrudan karşılığı vardır. Bir kavramın veya terimin sadece o bağlamda (context) belirli bir anlamı olduğu anlamına gelir. Farklı bağlamlarda aynı terim, farklı anlamlara veya farklı Model'lere sahip olabilir.

Örnek: "User" (Kullanıcı) Kavramı

Bir e-ticaret sistemini düşünelim:

  • Identity (Kimlik) Microservice (Mikroservis): Burada bir User Entity'si, kullanıcının Email (E-posta), PasswordHash, FullName (Tam Adı), NationalId (Kimlik Numarası) gibi tüm kimlik doğrulama ve profil bilgilerini içerebilir.

      // Identity Context - Kullanıcı detayları odaklı
      public class User
      {
          public int Id { get; set; }
          public string FullName { get; set; }
          public string Email { get; set; }
          public string NationalId { get; set; } // Kimlik bilgisi
          public string PasswordHash { get; set; }
          // ... Diğer kimlik bilgileri
      }
    
  • Ordering (Sipariş) Microservice: Sipariş mikroservisinin bir siparişi kimin verdiğini bilmesi gerekir, ancak kullanıcının tüm kimlik bilgilerine (şifresi, kimlik numarası vb.) ihtiyacı yoktur. Burada User yerine Buyer (Alıcı) adında bir Entity tanımlarız ve sadece siparişle ilgili bilgileri (örneğin UserId, FullName, ShippingAddress) içerir.

      // Ordering Context - Sipariş için sadece gerekli bilgiler
      public class Buyer
      {
          public int Id { get; set; } // Bu, Identity Context'teki User.Id ile eşleşir
          public string FullName { get; set; }
          // Siparişle ilgili diğer bilgiler, örn: PaymentMethod, PreferredShippingOption
      }
    

    Aynı kişi, iki farklı "Bounded Context"te iki farklı Entity olarak modellenmiştir. Her bağlam (Context) sadece kendi ihtiyaçlarını bilir ve kendi Domain Model'ini oluşturur. Bu, mikroservislerin independence (bağımsızlığını) ve loose coupling (gevşek bağlılığını) destekler.

🧠 Service (Hizmet): Orkestrasyon ve Çapraz Alan İş Mantığı

Service'ler, doğrudan bir varlığa ait olmayan veya birden fazla Aggregate'i ilgilendiren iş mantığını barındırmak için kullanılır. DDD'de iki ana Service türü bulunur: Domain Service (Alan Hizmeti) ve Application Service (Uygulama Hizmeti).

🔸 Domain Service: Domain Layer'daki (Alan Katmanı) İş Akışları

Domain Service'ler, belirli bir Aggregate'e ait olmayan ancak Domain Layer için önemli olan iş süreçlerini veya hesaplamaları gerçekleştirir. Genellikle stateless (durumsuz) olurlar ve Aggregate'ler arasında köprü görevi görebilirler.

  • Cross-Aggregate Interaction (Çoklu Aggregate Etkileşimi): Bir işlem birden fazla Aggregate'i ilgilendiriyorsa (örneğin, bir kullanıcının bakiyesinden düşüp bir siparişin ödemesini onaylama), bu tür bir mantık Domain Service'te yer alabilir.

  • Calculations and Policies (Hesaplamalar ve Politikalar): Karmaşık hesaplamalar veya birden fazla Aggregate'i etkileyen iş politikaları burada konumlandırılabilir.

Örnek (.NET):

public class PricingService
{
    public decimal CalculateDiscount(Order order)
    {
        // İndirim hesaplama gibi domain kuralı, doğrudan Order'ın içinde olmak zorunda değildir.
        if (order.TotalAmount > 1000)
            return order.TotalAmount * 0.1m;
        return 0;
    }
}

PricingService, belirli bir Order nesnesine bağlı olmayan, genel bir indirim hesaplama mantığını barındırır. Bu mantık, farklı siparişler veya senaryolar için tekrar kullanılabilir.

🔸 Application Service (Uygulama Hizmeti): Application Layer'daki (Uygulama Katmanı) Orkestrasyon

Application Service'ler, Domain Service'lerden farklı olarak doğrudan Application Layer'da yer alır. Kullanıcı isteklerini (Command'ları) alır, uygun Aggregate'leri veya Domain Service'leri çağırır, Repository'ler (Depo) aracılığıyla veritabanı işlemlerini koordine eder ve sonucu geri döndürür. Genellikle CQRS (Command Query Responsibility Segregation) deseninde Command Handler veya Query Handler olarak görev yaparlar.

  • Orchestration Role (Orkestrasyon Rolü): Uygulamanın dış dünyayla etkileşimini ve iç Domain Model'ini bir araya getiren bir orkestratör görevi görür. Doğrudan iş mantığı içermekten ziyade, mevcut Domain Object'lerini ve Service'leri kullanarak bir akışı yönetir.

  • Repository Interaction (Depo Etkileşimi): Veri erişim katmanıyla (Repository'ler) iletişim kurarak Aggregate'leri yükler ve kaydeder.

Örnek (.NET):

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand>
{
    private readonly IOrderRepository _orderRepository; // Repository bağımlılığı

    public CreateOrderHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public async Task Handle(CreateOrderCommand command, CancellationToken cancellationToken)
    {
        // Application Service, Aggregate'i oluşturur ve onun metodlarını çağırır.
        var shippingAddress = new Address(command.Street, command.City, command.ZipCode);
        var order = new Order(shippingAddress); 

        foreach (var item in command.Items)
        {
            order.AddItem(item.ProductId, item.ProductName, item.Price, item.Quantity);
        }

        // Aggregate'i repository aracılığıyla kalıcı hale getirir.
        await _orderRepository.AddAsync(order); 
    }
}

CreateOrderHandler bir Application Service'tir. Kullanıcının CreateOrderCommand'ını alır, Order Aggregate'ini oluşturur ve içindeki AddItem metodunu çağırır. Son olarak, Order Aggregate'ini bir Repository aracılığıyla veritabanına kaydeder. Burada işin nasıl yapılacağı (orkestrasyon) yönetilir.


🎯 Aggregate ve Service Arasındaki Temel Farklar

ÖzellikAggregate (Varlık Topluluğu)Service (Hizmet)
What Is It? (Nedir?)Domain Model içindeki Data + Behavior birleşimiBusiness Logic Carrier (İş Mantığı Taşıyıcısı) (Orkestrasyon veya çapraz kural)
State? (Durum?)Evet, kendi durumunu (verilerini) yönetir.Genellikle hayır, stateless'tir (durumsuz).
Where Does It Operate? (Nerede Çalışır?)Domain içinde, belirli bir varlığa odaklıdır.Domain Layer'da (Domain Service) veya Application Layer'da (Application Service)
When Is It Used? (Ne Zaman Kullanılır?)Nesnenin kendisiyle ilgili Business Rules (İş Kuralları) ve Consistency (Tutarlılık) sağlanacaksa.Birden fazla Aggregate'i etkileyen işlemler, sistem etkileşimleri, orkestrasyon veya karmaşık hesaplamalar varsa.

Bu ayrımı doğru yapmak, hem kodunuzun daha temiz ve anlaşılır olmasını sağlar hem de karmaşık Domain'lerde bakım ve genişletilebilirlik açısından büyük avantajlar sunar. Mikroservis mimarisinde, her mikroservisin kendi Aggregate'lerini ve bu Aggregate'leri yöneten Service'lerini barındırması, independence (bağımsızlık) ve encapsulation (kapsülleme) için kritik öneme sahiptir.

⚡️ CQRS (Command Query Responsibility Segregation) ile Entegrasyon

Rich Domain Model'ler ve DDD prensipleri, özellikle CQRS (Command Query Responsibility Segregation) mimarisiyle birlikte kullanıldığında tam potansiyelini ortaya koyar.

  • Command (Write) Side: Rich Domain Model'ler ve Aggregate'ler, genellikle CQRS'in Command (Write) Side'ında kullanılır. Kullanıcıdan gelen bir Command (Komut) (örneğin, "Sipariş Oluştur"), ilgili Aggregate Root'u yükler, iş mantığını Aggregate üzerinde çalıştırır ve Aggregate'in durumunu değiştirerek veritabanına kaydeder. Tüm iş kuralları ve tutarlılık Aggregate içinde sağlanır.

  • Query (Read) Side: Okuma işlemleri (veri sorgulama) için ise daha basit, optimize edilmiş ve genellikle Anemic Data Model'ler (DTO'lar - Data Transfer Objects) kullanılır. Bu taraf, doğrudan veritabanından veri okuyabilir ve karmaşık Domain Logic (Alan Mantığı)'na ihtiyaç duymaz. Okuma modelleri, belirli ekran veya rapor ihtiyaçlarına göre denormalize edilebilir. Bu sayede, okuma performansından ödün vermeden, yazma tarafındaki karmaşık Domain Model'i koruyabiliriz.

Bu ayrım, her iki tarafın bağımsız olarak ölçeklenmesine ve optimize edilmesine olanak tanır.


💬 Sıkça Sorulan Sorular (SSS)

1. Her mikroserviste Rich Domain Model kullanmak zorunda mıyım?

Hayır. Eğer mikroservisiniz sadece basit CRUD (Create, Read, Update, Delete) işlemleri yapıyorsa ve karmaşık bir iş mantığı içermiyorsa, Anemic Model yeterli olabilir. Ancak Business Domain'inizde (İş Alanı) karmaşık kurallar ve davranışlar varsa, Rich Domain Model uzun vadede size büyük faydalar sağlayacaktır.

2. Aggregate'ler mikroservis sınırları içinde mi kalmalı?

Evet, genellikle bir Aggregate, tek bir mikroservisin (veya Bounded Context'in) içindeki Consistency Boundary'yi (Tutarlılık Sınırı) temsil eder. Bir Aggregate'i birden fazla mikroservise yaymak, dağıtık işlem karmaşasına ve tutarlılık sorunlarına yol açabilir.

3. Application Service ile Domain Service arasındaki farkı nasıl daha iyi anlarım?

  • Application Service: "What is being done?" (Ne yapılıyor?) sorusuna cevap verir. Dış dünya ile Domain arasındaki orkestrasyonu sağlar (örneğin "Yeni Sipariş Oluştur").

  • Domain Service: "How is it done according to business rules?" (İş kurallarına göre nasıl yapılıyor?) sorusuna cevap verir. Birden fazla Aggregate'i içeren veya tek bir Aggregate'e ait olmayan Domain Logic'i barındırır (örneğin "İndirim Hesapla").

4. DDD'yi öğrenmek zor mu?

DDD, başlangıçta karmaşık görünebilir çünkü sadece teknik bir desen değil, aynı zamanda bir düşünce yapısıdır. Ancak temel prensipleri (Bounded Context, Aggregate, Value Object vb.) anladıkça, karmaşık iş alanlarını modelleme yeteneğiniz önemli ölçüde gelişecektir. Pratik yaparak ve küçük adımlarla başlayarak ustalaşabilirsiniz.


🔚 Sonuç

Mikroservis mimarileri, doğru yaklaşımla uygulandığında yazılım geliştirme sürecine büyük avantajlar sağlar. Domain-Driven Design ve Rich Domain Model'ler ise bu avantajları maksimuma çıkarmak için kritik bir araç setidir. İş kurallarınızı ve davranışlarınızı doğrudan Domain Object'lerinizin içine yerleştirmek, kodunuzu daha okunabilir, test edilebilir ve uzun vadede bakımı kolay hale getirir.

Unutmayın, "Anemic Model"ler basitlik vaat etse de, karmaşık Domain'lerde sizi hızla teknik borca ve yönetilemez bir kod tabanına sürükleyebilir. Rich Domain Model'ler ile, karmaşıklığı ehlileştirebilir, işinizi kodunuzda açıkça ifade edebilir ve daha sağlam, tutarlı mikroservisler inşa edebilirsiniz.

Peki, siz projelerinizde DDD prensiplerini uygularken hangi zorluklarla karşılaşıyor veya hangi faydaları görüyorsunuz? Yorumlarda düşüncelerinizi paylaşmaktan çekinmeyin!

0
Subscribe to my newsletter

Read articles from Uygar Öztürk Ceylan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Uygar Öztürk Ceylan
Uygar Öztürk Ceylan