Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi Domain-Driven Design — #14

DDD'nin Evrimi ve Yaygın Yanlışlar: Blue Book'tan Dynamic Consistency Boundary'ye

Mert Özen 7 Kas 2026 13 dk 4 görüntülenme
DDD'nin Evrimi ve Yaygın Yanlışlar: Blue Book'tan Dynamic Consistency Boundary'ye

DDD'nin en yaygın yanlışı, Blue Book'un sonunda kalan strategic design'ı atlayıp yalnızca entity, repository ve base class gibi tactical kelimeleri almaktır. Evans'ın 2003'teki kitabından DDD Reference'a, Vernon'ın aggregate kurallarına, EventStorming'e, Team Topologies ve Khononov'a, aggregate'i sorgulayan Dynamic Consistency Boundary'ye uzanan tarihçe; bellek içi bir DCB örneği ve seriden sonraki okuma rotası.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

Bir iş görüşmesinde "projemizde DDD (Domain-Driven Design) kullanıyoruz" diyen bir ekibin repository'sini açtığını düşün. Domain adında bir project var, içinde şunlar:

namespace Bookstore.Domain.Common
{
    public interface IAggregateRoot;

    public abstract class Entity<TId>
    {
        public TId Id { get; set; } = default!;
        public DateTime CreatedAt { get; set; }
        public DateTime? UpdatedAt { get; set; }
    }

    public interface IRepository<T> where T : Entity<int>, IAggregateRoot
    {
        Task<T?> GetByIdAsync(int id);
        Task<List<T>> GetAllAsync();
        void Add(T entity);
        void Update(T entity);
        void Delete(T entity);
    }
}

namespace Bookstore.Domain.Entities
{
    using Bookstore.Domain.Common;

    public class Customer : Entity<int>, IAggregateRoot
    {
        public string Email { get; set; } = "";
        public List<Order> Orders { get; set; } = [];
    }

    public class Book : Entity<int>, IAggregateRoot
    {
        public string Title { get; set; } = "";
        public decimal Price { get; set; }
        public int Stock { get; set; }
    }

    public class Order : Entity<int>, IAggregateRoot
    {
        public Customer Customer { get; set; } = null!;
        public string Status { get; set; } = "New";
        public decimal Total { get; set; }
        public List<OrderItem> Items { get; set; } = [];
    }

    public class OrderItem : Entity<int>, IAggregateRoot
    {
        public Order Order { get; set; } = null!;
        public Book Book { get; set; } = null!;
        public int Quantity { get; set; }
    }
}

Bu kodda DDD'den ne var, ne yok? Kaç aggregate var? OrderItem'ın kendi repository'si olması neyi anlatır? Ve en önemlisi, bu kodu okuyarak Bookstore'un hangi kuralları olduğunu öğrenebilir misin?

Kelimeler Var, Model Yok

Tahmin yerine küçük bir reflection script'iyle saydım:

using System.Reflection;
using Bookstore.Domain.Common;

// Basit bir audit: aggregate root sayısı, aggregate'ler arası navigation, public setter
Type[] entities = [.. typeof(IAggregateRoot).Assembly.GetTypes()
    .Where(t => t is { IsClass: true, IsAbstract: false } && t.Namespace == "Bookstore.Domain.Entities")];
Type[] roots = [.. entities.Where(t => typeof(IAggregateRoot).IsAssignableFrom(t))];
Console.WriteLine($"Aggregate roots: {roots.Length} of {entities.Length} entities");

foreach (Type type in entities)
{
    foreach (PropertyInfo property in type.GetProperties())
    {
        Type target = property.PropertyType.IsGenericType
            ? property.PropertyType.GetGenericArguments()[0]
            : property.PropertyType;
        if (roots.Contains(target))
            Console.WriteLine($"Navigation to another aggregate: {type.Name}.{property.Name} -> {target.Name}");
    }
}

int setters = entities.SelectMany(t => t.GetProperties()).Count(p => p.SetMethod?.IsPublic == true);
Console.WriteLine($"Public setters: {setters}");
Console.WriteLine($"Domain methods: {entities.SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)).Count(m => !m.IsSpecialName)}");
Aggregate roots: 4 of 4 entities
Navigation to another aggregate: Customer.Orders -> Order
Navigation to another aggregate: Order.Customer -> Customer
Navigation to another aggregate: Order.Items -> OrderItem
Navigation to another aggregate: OrderItem.Order -> Order
Navigation to another aggregate: OrderItem.Book -> Book
Public setters: 24
Domain methods: 0

Dört entity'nin dördü aggregate root. OrderItem kendi başına bir aggregate, ama Order'a navigation ile bağlı; Order da Customer'a. Bir siparişi yüklemek müşteriyi, müşteri bütün siparişlerini getiriyor; consistency boundary diye bir şey yok. 24 public setter var ve tek bir domain method'u yok. Kodda DDD'nin kelimeleri (Entity, IAggregateRoot, IRepository) var ama modeli yok: kurallar başka bir yerde, büyük ihtimalle service'lerde. Bu, DDD'nin yirmi yılı aşkın tarihinde en sık rastlanan yanlış anlamanın kısa bir özeti ve bu yazı onun nereden geldiğini anlatıyor.

2003: Blue Book ve Sondaki Strateji

Eric Evans'ın Domain-Driven Design: Tackling Complexity in the Heart of Software kitabı Ağustos 2003'te çıktı; künyesinde 2004 yazdığı için Evans da ondan "2004 kitabı" diye bahseder, okurlar ise kapağının renginden "Blue Book" der. Kitabın dört bölümü var: domain modelini işe koşmak (ubiquitous language, model ile kodun birliği), model'in building block'ları (entity, value object, service, aggregate, factory, repository), refactoring toward deeper insight ve strategic design (bounded context, context map, distillation, core domain).

Sorun sıralamada. Strategic design kitabın son çeyreğinde ve okurların büyük kısmı building block'larda durdu. Sonuç, yukarıdaki kod: DDD bir class kataloğuna, "entity base class + generic repository" şablonuna indirgendi. Evans 2009'da QCon London'daki "kitaptan beri ne öğrendim" konuşmasında bunu kendisi söyledi: building block'lar önemli ama fazla öne çıkmıştı, asıl kazanç core domain'e odaklanmaktan geliyordu. Serinin 01. yazısında strategic ve tactical ayrımını bu yüzden en başa koymuştuk.

Evans'ın Kendi Düzeltmeleri: DDD Reference

Evans 2015'te kitabın tanımlarını ve pattern'lerin kısa açıklamalarını DDD Reference adıyla, Creative Commons lisansıyla ücretsiz yayımladı. İçinde kitapta olmayan üç pattern var ve hepsi yıldızla işaretli: Domain Events, Partnership ve Big Ball of Mud. Domain event'i serinin 12. yazısında, Partnership'i ve Big Ball of Mud'ı 05. yazıdaki context map'te kullandık. Big Ball of Mud'ın eklenmesi özellikle dürüst bir kabul: büyük bir sistemin her yeri iyi tasarlanmış olmayacak ve bu, haritada bir yere sahip olmalı.

Reference'ın teşekkür bölümü de kısa bir tarihçe gibi okunuyor. Evans, CQRS (Command Query Responsibility Segregation) ve Event Sourcing'i DDD sistemleri için ana akım mimari seçeneklere dönüştürenler olarak Greg Young ve Udi Dahan'ı anıyor; kitabından sonraki ilk DDD kitabını Jimmy Nilsson'ın yazdığını, Vaughn Vernon'ın kitabının da kendisininkinden bu yana en iddialısı olduğunu söylüyor. CQRS ve Event Sourcing ilerideki ayrı bir serinin konusu.

Vernon ve Aggregate'in Dört Kuralı

Vaughn Vernon'ın 2011'de üç parça hâlinde yayımladığı Effective Aggregate Design makaleleri, Blue Book'taki en çok yanlış anlaşılan building block'una somut kurallar getirdi: gerçek invariant'ları consistency boundary'nin içine al, aggregate'leri küçük tasarla, diğer aggregate'lere id ile referans ver, sınırın dışında eventual consistency kullan. Açılıştaki kod dört kuralın da tersini yapıyor. Vernon 2013'te Implementing Domain-Driven Design'ı (kapağından dolayı "Red Book"), 2016'da kısa bir giriş olan Domain-Driven Design Distilled'ı yayımladı. Serinin 09. yazısında Order aggregate'ini bu dört kurala göre çizmiştik.

Modeli Birlikte Keşfetmek: EventStorming ve DDD Crew

Blue Book "domain expert ile birlikte çalış" diyordu ama bunun nasıl yapılacağını pek göstermiyordu. Alberto Brandolini Kasım 2013'te bir blog yazısıyla EventStorming'i tanıttı: geniş bir duvar, turuncu sticky'ler ve geçmiş zamanlı domain event'lerle başlayan bir atölye. İlk adı "Event-based modelling", Brandolini'nin konuk eğitmen olduğu Vaughn Vernon'ın IDDD (Implementing Domain-Driven Design) eğitim turundan geliyor; Brandolini'nin kitabı Introducing EventStorming Leanpub'da yayımlanıyor. Aynı dönemde başka teknikler de çıktı: Matt Wynne'in Example Mapping'i (2015) kuralları örneklerle konuşmak için, Stefan Hofer ve Henning Schwentner'ın Domain Storytelling'i (kitabı 2021) aktörlerin anlattığı hikâyelerden model çıkarmak için. Serinin 06. yazısındaki EventStorming duvarı bu geleneğin parçası.

Sonraki adım, bu atölyelerin çıktısını tekrarlanabilir şablonlara dökmekti. GitHub'daki DDD Crew topluluğu Bounded Context Canvas'ı (Nick Tune'un çalışmasından), Context Mapping pattern kartlarını, Aggregate Design Canvas'ı ve DDD Starter Modelling Process'i açık kaynak olarak topladı. Serinin 04. yazısında Ordering'i Bounded Context Canvas ile, 09. yazısında aggregate'i Aggregate Design Canvas'ın sorularıyla tanımlamıştık.

Microservices, Team Topologies ve Khononov

2014'te James Lewis ve Martin Fowler'ın "Microservices" makalesi, servis sınırları için DDD'nin bounded context'ini referans gösterdi; Sam Newman'ın Building Microservices (2015) kitabı bunu bir tasarım yöntemine çevirdi. DDD bir anda mikroservis tasarımının dili oldu, ama beraberinde bir yanlış anlama da getirdi: "DDD yapıyorsak microservice'e geçmeliyiz." Bounded context bir model sınırıdır; aynı process'te bir namespace ya da module olarak da yaşayabilir. Bookstore bugün tek deployment unit'li bir monolith ve serinin bütün örnekleri bu monolith'in içinde çalıştı.

Matthew Skelton ve Manuel Pais'in Team Topologies (2019) kitabı ilişkiyi ekip tarafından kurdu: stream-aligned ekiplerin sorumluluk sınırlarını çizerken kullanılan "fracture plane"lerin başında business domain'in bounded context'leri geliyor. Bunun Conway's Law ile bağlantısını Mimariye Giriş serisinin "Conway's Law: Ekip Yapısı Mimariyi Nasıl Şekillendirir?" yazısında konuşmuştuk; Bookstore'un 12–18 ay sonraki dört ekibi bu fikirle çizildi. Vlad Khononov'un Learning Domain-Driven Design (2021) kitabı ise strategic design'ı yeni başlayanlar için en okunabilir hâline getirdi: core, supporting ve generic subdomain'leri rekabet avantajı ve karmaşıklıkla ölçmek, her türe farklı bir uygulama stratejisi seçmek. Serinin 03. yazısı büyük ölçüde bu çerçeveye dayanıyor. Khononov'un 2024'teki Balancing Coupling in Software Design kitabı aynı soruyu coupling üzerinden yeniden soruyor.

Aggregate'i Sorgulamak: Dynamic Consistency Boundary

Yirmi yılın en yeni tartışması, aggregate'in kendisine itiraz. Sara Pellegrini Nisan 2023'te "I am here to kill the aggregate" başlıklı yazısıyla başlayan seride, aggregate'in tanımının belirsiz olduğunu ve yanlış çizilen bir sınırın sonradan düzeltilmesinin çok pahalı olduğunu savundu. Önerisi Dynamic Consistency Boundary (DCB): consistency boundary'yi tasarım anında sabit bir aggregate olarak değil, her karar için o kararın okuduğu event'leri seçen bir sorgu olarak tanımlamak. Fikir sonradan dcb.events adresinde bir spesifikasyona dönüştü.

Bookstore'dan bir örnek: imzalı baskılar. Her baskıdan iki kopya var ve bir müşteri bütün baskılardan en fazla iki imzalı kopya alabilir. Kural iki "varlık"a yayılıyor. Aggregate ile modellemek istersen ya her şeyi tek bir dev aggregate'e koyarsın (bütün rezervasyonlar aynı satırda çakışır) ya da Edition ve Customer'ı ayrı aggregate'ler yapıp bir rezervasyonu iki ayrı transaction'a ve bir compensation akışına bölersin. DCB'de tek bir event yazılır ve iki tag taşır. Spesifikasyonun istediği iki yeteneği (tipe ve tag'e göre okuma, conditional append) bellek içinde 35 satırlık bir dosyayla kurdum:

namespace Bookstore.Dcb;

public sealed record SequencedEvent(long Position, object Event, IReadOnlySet<string> Tags);

// Spesifikasyondaki Query Item: tiplerden biri VE etiketlerin hepsi
public sealed record QueryItem(Type[] Types, string[] Tags)
{
    public bool Matches(SequencedEvent e) =>
        (Types.Length == 0 || Types.Contains(e.Event.GetType())) && Tags.All(e.Tags.Contains);
}

// Query: item'lardan herhangi biri eşleşirse
public sealed record Query(params QueryItem[] Items)
{
    public bool Matches(SequencedEvent e) => Items.Any(item => item.Matches(e));
}

public sealed record AppendCondition(Query FailIfEventsMatch, long After);

public sealed class AppendConditionFailedException(string message) : Exception(message);

public sealed class InMemoryEventStore
{
    private readonly List<SequencedEvent> _events = [];
    private readonly Lock _gate = new();

    public (SequencedEvent[] Events, long Head) Read(Query query)
    {
        lock (_gate)
            return ([.. _events.Where(query.Matches)], _events.Count);
    }

    public void Append(object domainEvent, string[] tags, AppendCondition condition)
    {
        lock (_gate)
        {
            // Okuduktan sonra aynı sorguya uyan bir event eklendiyse karar bayatlamıştır
            if (_events.Skip((int)condition.After).Any(condition.FailIfEventsMatch.Matches))
                throw new AppendConditionFailedException("A matching event was appended after the decision was read.");
            _events.Add(new SequencedEvent(_events.Count + 1, domainEvent, tags.ToHashSet()));
        }
    }
}
// İki kural, iki "varlık": baskı başına kopya sayısı ve müşteri başına imzalı kopya sınırı
const int CopiesPerEdition = 2;
const int MaxSignedCopiesPerCustomer = 2;

Decision Decide(string edition, string customer)
{
    Query query = new(
        new QueryItem([typeof(SignedCopyReserved)], [$"edition:{edition}"]),
        new QueryItem([typeof(SignedCopyReserved)], [$"customer:{customer}"]));
    (SequencedEvent[] events, long head) = store.Read(query);

    int sold = events.Count(e => e.Tags.Contains($"edition:{edition}"));
    int owned = events.Count(e => e.Tags.Contains($"customer:{customer}"));
    string? rejection = sold >= CopiesPerEdition ? "sold out"
        : owned >= MaxSignedCopiesPerCustomer ? "customer limit reached"
        : null;
    return new Decision(edition, customer, rejection, new AppendCondition(query, head));
}

void Commit(Decision decision)
{
    if (decision.Rejection is not null)
    {
        Console.WriteLine($"{decision.Customer,-6} {decision.Edition,-12} rejected: {decision.Rejection}");
        return;
    }
    store.Append(new SignedCopyReserved(decision.Edition, decision.Customer),
        [$"edition:{decision.Edition}", $"customer:{decision.Customer}"], decision.Condition);
    Console.WriteLine($"{decision.Customer,-6} {decision.Edition,-12} reserved");
}
Commit(Decide("dune-signed", "ada"));
Commit(Decide("emma-signed", "ada"));
Commit(Decide("dune-signed", "ada"));

// Alan ve Grace son Dune kopyasına aynı anda karar veriyor; Linus başka bir baskıya
Decision alan = Decide("dune-signed", "alan");
Decision grace = Decide("dune-signed", "grace");
Decision linus = Decide("emma-signed", "linus");
Commit(alan);
try
{
    Commit(grace);
}
catch (AppendConditionFailedException ex)
{
    Console.WriteLine($"grace  dune-signed  conflict: {ex.Message}");
    Commit(Decide("dune-signed", "grace"));
}
Commit(linus);
ada    dune-signed  reserved
ada    emma-signed  reserved
ada    dune-signed  rejected: customer limit reached
alan   dune-signed  reserved
grace  dune-signed  conflict: A matching event was appended after the decision was read.
grace  dune-signed  rejected: sold out
linus  emma-signed  reserved

Ada iki baskıdan birer kopya aldı, üçüncü denemesi müşteri sınırına takıldı. Alan ve Grace son Dune kopyası için aynı anda karar verdi; Alan'ın event'i Grace'in sorgusuna uyduğu için Grace'in ekleme koşulu bozuldu, yeniden okuyunca kopyanın bittiğini gördü. Linus da aynı anlarda karar vermişti, ama onun sorgusu Emma baskısını ve kendisini kapsıyordu; Alan'ın event'i ona dokunmadığı için çakışma olmadı. Consistency boundary, kararın kendi sorgusu kadar büyük.

Bedeli de açık. DCB tag ve conditional append destekleyen bir event store ister; Entity Framework Core (EF Core) ile ilişkisel tablolar üzerine kurduğumuz bu serinin modeline doğrudan uymaz. Spesifikasyonu uygulayan .NET kütüphaneleri var ama genç; Java tarafında Axon Framework 5 desteği getirdi. Her karar kendi sorgusunu tasarlamayı gerektirir ve "bu sorgu doğru event'leri kapsıyor mu" sorusu, eskiden aggregate sınırının cevapladığı soruyu her komuta dağıtır. Event store ve event-sourced modeller ilerideki CQRS ve Event Sourcing serisinin konusu; DCB'yi orada daha ayrıntılı ele almak gerekecek. Buradan çıkarılacak ders aggregate'in öldüğü değil, onun bir amaç değil bir araç olduğu: amaç invariant'ları korumak.

Sık Düşülen Tuzaklar: DDD'nin Yaygın Yanlışları

Yalnızca tactical DDD

Açılıştaki kodun hikâyesi. Strategic design olmadan tactical pattern'ler, CRUD (Create, Read, Update, Delete) koduna eklenmiş törenlerdir. Bounded context'i, ubiquitous language'i ve core domain'i belli olmayan bir projede IAggregateRoot interface'i bir şey anlatmaz.

Entity başına bounded context ya da entity başına aggregate

Bounded context bir modelin ve dilinin geçerli olduğu sınırdır; bir tablo, bir entity ya da bir controller değildir. Aynı şekilde her entity'yi aggregate root yapmak, aggregate kavramını tamamen boşaltır. Ölçü invariant: "Bu iki şey her zaman birlikte tutarlı olmak zorunda mı?" sorusunun cevabı hayırsa ayrı aggregate'tirler, evetse aynı.

Ubiquitous language'i atlamak

Kodda OrderDetail, toplantıda "sipariş satırı", destek ekibinde "ürün" deniyorsa, model zaten üçe bölünmüştür. 13. yazıdaki ilk commit'in yalnızca isim değiştirmesi bu yüzdendi. Türk ekipler için kod İngilizce, domain dili Türkçe olduğunda glossary'nin çeviri tablosu dilin kendisidir (02. yazı).

DDD'yi microservice zorunluluğu sanmak

Bounded context'i microservice'e eşitlemek, iki ayrı kararı birbirine bağlar: modelin sınırı ve deployment'ın sınırı. İlki her zaman gereklidir, ikincisi ölçek ve ekip yapısı gerektirdiğinde. İlerideki Modular Monolith serisi bounded context'leri aynı process'te ayrı module olarak tutmayı, Microservices serisi de ne zaman ayrı deployment'a dönüştürmenin değdiğini ele alacak.

Generic repository ve base class kültü

IRepository<T> içinde Update ve Delete olan her entity için açılan generic repository, aggregate sınırını deler: OrderItem'ı Order'dan bağımsız silmek mümkün hâle gelir. Repository aggregate başına ve aggregate'in diliyle tanımlanır (10. yazı). Entity<TId> base class'ı da kendi başına bir kazanç değildir; içine audit field'ları, soft delete, tenant id eklendikçe bütün domain'i bağlayan bir infrastructure class'ına dönüşür.

Her projeye DDD

Kuralları az, değişkenliği düşük bir supporting ya da generic subdomain'de rich model ve event'ler kurmak maliyettir. Evans'ın kitabının alt başlığı bir uyarıdır: karmaşıklık yazılımın kalbinde, yani core domain'de. Oraya yatırım yap, gerisini basit tut.

Seriden Sonra: Devam Rotası

On dört yazıda önce problemi (ubiquitous language, subdomain, bounded context, context map, EventStorming), sonra çözümün building block'larını (entity, value object, aggregate, persistence, service, domain event) ve son olarak bir dönüşümü uçtan uca gördük. Buradan dört yol ayrılıyor. İlerideki CQRS ve Event Sourcing serisi okuma ve yazma modellerini ayırmayı, aggregate'i event'lerden kurmayı ve DCB'yi; Messaging ve Event-Driven Architecture serisi integration event'lerini, outbox'ı ve saga'ları; Modular Monolith serisi bounded context'leri aynı process'te ayrı module'lere dönüştürmeyi; Microservices serisi de ne zaman ve nasıl ayrı servis yapmanın değdiğini anlatacak. Kitap olarak sıralama önerim: önce Khononov'un Learning Domain-Driven Design'ı, sonra Evans'ın Blue Book'u (strategic design bölümüyle başlayarak), sonra Vernon'ın Effective Aggregate Design makaleleri ve ücretsiz DDD Reference.

Küçük Bir Deneme

★☆☆ Açılıştaki audit script'ini kendi projenin domain assembly'si üzerinde çalıştır. Kaç aggregate root, kaç aggregate'ler arası navigation ve kaç domain method'u çıkıyor?

★★☆ DCB örneğine bir kural ekle: "Bir baskının ilk 24 saatinde yalnızca sadakat programı üyeleri imzalı kopya alabilir." Hangi event'lere ve hangi tag'lere ihtiyacın var? Karar sorgusu nasıl değişiyor?

★★★ Aynı imzalı baskı kuralını Edition ve Customer aggregate'leriyle, 12. yazıdaki domain event'lerle ve eventual consistency ile modelle. Alan ve Grace'in aynı anda son kopyayı istediği senaryoda hangi compensation akışı gerekiyor? DCB çözümüyle satır satır karşılaştır.

Bu yazıyla Domain-Driven Design serisi bitiyor. Bookstore'un Ordering context'i artık kendi dilini konuşan, invariant'larını koruyan ve olanı event olarak söyleyen bir modele sahip. Bir sonraki adım, bu event'leri context'ler arasında güvenilir şekilde taşımak; o yolculuk ilerideki Messaging ve Event-Driven Architecture serisinde başlıyor.