Taktický Domain Driven Design nefunguje

10 min

TLDR: Najít aplikaci, pro kterou se taktické DDD vyplatí, je opravdu obtížné a dost možná i nemožné. V drtivé většině případů DDD pouze přidá více práce než užitku.

Taktický Domain Driven Design (DDD) zní na papíře skvěle - modelujete doménu pomocí agregátů a entit, hlídáte invarianty a váš kód dokonale zrcadlí byznys. Jenže jakmile začnete psát první řádky kódu, tak začnete narážet na spoustu velice praktických problémů. Pojďme se na několik z nich pro demonstraci podívat.

Pokud si chcete připomenout DDD nebo nevíte, co to je, doporučuji předchozí článek.

Problém 1: Ukládání agregátů

Cílem DDD je reprezentovat vaši doménu co nejlépe a neomezovat se SQL tabulkami. Přirozeně se tedy váš doménový model rozejde s tím databázovým a musíte řešit, jak namapovat agregáty na SQL tabulky. Obecně máte několik možností:

  • Ruční mapování - napíšete mapovací kód, který převádí databázový model na doménový a naopak
  • ORM - vytvoříte doménový model a ten pak pomocí automatizovaného nástroje namapujete na databázi (Prisma, SQLAlchemy, Entity Framework, Hibernate, GORM)
  • Použití NoSQL - agregáty serializujete a uložíte do dokumentové databáze (MongoDB)
  • Další varianty - Event sourcing, Memento pattern, atd.

Všechny možnosti jsou použitelné, ale každá má své nevýhody.

Nevýhody jednotlivých způsobů ukládání

Ruční mapování

  • Velice komplikované u jakékoliv složitější aplikace
  • Při každé změně musíte myslet na aktualizaci mapování. Často se stává, že přidáte nový field, ale zapomenete ho namapovat, což vede k chybějícím datům
  • Boilerplate kód - trávíte čas psaním a testováním nudného kódu pro přelévání dat z levé kapsy do pravé místo řešení reálných problémů

ORM

ORM fungují nejlépe, pokud vaše třídy 1:1 odpovídají databázovým tabulkám a nepoužívají privátní stav, což je přesný opak toho, co potřebujete v DDD. V předchozím článku o DDD jsem ukazoval jednoduchý příklad agregátu:

// constructor omitted for brevity
public class User
{
  public int Id { get; private set;}  
  public bool IsPremium { get; private set; }
  private Cart Cart { get; set; }

  public void AddProductToCart(Product product)
  {
    if (Cart.Products.Count == 3 && !IsPremium)
    {
      throw new InvalidOperationException("User can not add more than 3 products to cart");
    }

    Cart.Products.Add(product);
  }
}

I tento relativně jednoduchý příklad bude obtížné namapovat pomocí většiny ORM.

Nakonec jste vždy nuceni vaše agregáty zjednodušit tak, aby se daly mapovat. Tím ale značně snížíte výhody, které vám DDD přináší.

Dalším problémem je, že nevíte, co všechno ORM dokáže. Často se vám stane, že vymyslíte krásný agregát, se kterým se výtečně pracuje, jen abyste ho pak několik hodin zjednodušovali a snažili se namapovat pomocí ORM.

Pokud chcete vědět více o ORM vs ručním mapování, doporučuji tento článek

Dokumentové databáze

Nejlepším řešením ukládání je dokumentová databáze (MongoDB a podobné) - celý agregát jednoduše serializujete a uložíte. Kolem roku 2013 byla dokumentová databáze představena jako ideální řešení. Martin Fowler psal:

As you might have gathered, I think NoSQL is technology to be taken very seriously. If you have an application problem that maps well to a NoSQL data model - such as aggregates or graphs - then you can avoid the nastiness of mapping completely.

Jelikož je dnes rok 2025, tak asi víte, že dokumentové databáze se moc často nepoužívají jako náhrada SQL. Důvodů je spousta, ale jednoduše můžeme říct, že použitím NoSQL vyřešíte mapování agregátů, ale přinesete do aplikace jinou sadu komplikací, které musíte řešit.

Event sourcing

Vysvětlení Event Sourcingu je nad rámec tohoto článku. Pokud Event Sourcing neznáte, doporučuji přednášku od Grega Younga.

Event Sourcing se stal velice populárním v DDD komunitě, jelikož elegantně řeší ukládání agregátů do databáze. Agregáty si můžete představit jako write model, který místo aktuálního stavu ukládá pouze události. Oddělený read model pak zajišťuje, že nejste nijak omezeni agregáty a můžete data číst podle potřeby.

Event Sourcing ale přináší velké množství komplikací a je vhodný pouze tehdy, pokud máte specifický důvod pro jeho použití. [0][1][2] Příkladem mohou být finanční aplikace, které přirozeně pracují s transakcemi peněz. Dalším zajímavým použitím je například LMAX architektura.

Mezi nevýhody Event Sourcingu patří:

  • Obrovská komplexita – ruční mapování mezi doménovým a databázovým modelem je velice pracné. Event Sourcing je ještě pracnější, jelikož musíte agregáty překládat na eventy a z eventů pak vytvářet read modely.
  • Verzování eventů – jak vyřešíte situace, kdy potřebujete přidat novou verzi eventu? Bude vaše aplikace donekonečna umět pracovat se starou i novou verzí? Tento problém je tak velký, že existuje celá kniha na toto téma.
  • Některé operace jsou mnohem obtížnější na řešení – jak budete validovat unikátnost uživatelského jména?
  • Performance – pokud máte větší množství eventů, musíte řešit snapshotting. Snapshotting je sám o sobě komplikovaný a dále komplikuje například verzování eventů.
  • Blue-green deployment – pokud chcete, aby vám nějakou dobu běžela stará i nová verze aplikace, musíte (mimo jiné) zajistit forward compatibility, jelikož nová verze aplikace může vytvořit event, který ta stará nezná.
  • Ad hoc queries – pokud chcete narychlo zjistit údaj z databáze, může to být komplikované.
  • A další [0][1][2][3]

Problém 2: Velké agregáty

Při implementaci DDD se vám běžně bude stávat, že pro fungování agregátu budete potřebovat načíst z databáze velké množství tabulek. Často potřebujete pouze změnit jeden údaj a kvůli tomu musíte načíst X tabulek pro sestavení agregátu. V jiných případech agregát pro svou validaci potřebuje enormní množství dat - např. všechny produkty, které váš e-shop obsahuje.

V DDD se často doporučuje pro řešení těchto problémů použít ORM s lazy loadingem - ORM načte pouze aggregate root a referencované entity jsou donačteny, až když je použijete. Teoreticky je lazy loading skvělým řešením, ale v praxi přináší mnoho problémů a spousta ORM od lazy loadingu upouští:

  • .NET (Entity Framework Core) - stará verze (Entity Framework) měla lazy loading zapnutý defaultně. Nová verze (EF Core) používá defaultně eager loading.
  • Typescript (Prisma) - nepodporuje lazy loading
  • GO (GORM) - nepodporuje lazy loading

Všimněte si také, že lazy loading není možné použít s NoSQL databází nebo v situaci, kdy ručně mapujete mezi modely.

Problém 3: Rozdělení agregátu

Jelikož DDD říká, že máte dávat logiku do entit a value objektů, tak se vám může stát, že některé složité entity budou obsahovat stovky nebo i tisíce řádků logiky. DDD nenabízí žádnou jednoduchou možnost, jak tyto složité třídy rozdělit. Logika je totiž podle DDD na správném místě - to, že jí je hodně, už DDD moc nezajímá.

Problém 4: Validace a invarianty

Velkým tématem v komunitě DDD jsou validace a invarianty:

  • Invarianty musíte kontrolovat při vytvoření aggregate rootu, abyste zajistili, že nikdy nebude vytvořen v nevalidním stavu.
  • Validace se dějí na hraně systému - přijde vám request na API a vy musíte zkontrolovat, zda uživatel zadal jméno, email atd.

Invarianty a validace jsou často stejné (uživatel musí mít jméno, uživatel musí zaškrtnout podmínky atd.). Jak budete sdílet jejich kód?

Představte si agregát uživatele:

public class User
{
  public User(string firstName, string secondName)
  {
    if(firstName.Length == 0)
    {
      throw new ArgumentException("First name must have length > 0");
    }
    
    if(secondName.Length == 0)
    {
      throw new ArgumentException("Second name must have length > 0");
    }
  }
}

Toto je nepoužitelné pro validace, jelikož uživateli vždy zobrazíme jednu chybu, což je dost otravné. Můžeme tedy udělat:

public class User
{
  public User(string firstName, string secondName)
  {
    var errors = new List<string>();
    if(firstName.Length == 0)
    {
      errors.Add("First name must have length > 0");
    }
    
    if(secondName.Length == 0)
    {
      errors.Add("Second name must have length > 0");
    }
    
    if(errors.Count > 0)
    {
      throw new ArgumentException(string.Join(",", errors));
    }
  }
}

Ok, to je lepší. Jak ale namapujeme tuto chybu na vstupní parametry? Řekněme, že uživatel vidí input s Id “name” a “secondName”. Potřebujeme teď zobrazit:

  • U inputu “name” – “First name must have length > 0”
  • U inputu “secondName” – “Second name must have length > 0”

Jak ale toto mapování provedeme? Celé řešení je komplikované a liší se jazyk od jazyka. Pokud chcete vidět celé řešení pro ASP.NET Core, doporučuji článek zde.

Článek o validaci je krásná ukázka “zjednodušení”, které DDD přináší – zjednodušili jste vaši doménu a teď jste nuceni číst článek, který má cca 2000 slov, jen abyste věděli, jak vyřešit problém, o kterém jste doteď nevěděli, že ho máte.

Problém 5: Always valid domain model

DDD říká, že invarianty by nikdy neměly být porušeny. Co ale budete dělat, pokud:

  • Máte v databázi stará data, která neodpovídají novým požadavkům na invarianty? Například až po 2 letech běhu softwaru se rozhodnete, že vaši uživatelé musí zadat příjmení.
  • Máte vícekrokový proces, který musíte dočasně uložit do databáze? Vytvoříte agregát, který je pouze napůl naplněný?

Pokud vás zajímá řešení, můžete si přečíst další článek.

Další problémy DDD

A to je jen špička ledovce…

Výše uvedené problémy představují pouze ty nejčastější překážky, na které narazí prakticky každý. V reálné implementaci vás čekají další specifická úskalí daná vaším technologickým stackem a konkrétní doménou.

A další komplikace…

Pro úplnost zmíním, že jsem záměrně vynechal celou řadu dalších témat, abych článek zkrátil. Pro kompletní DDD byste museli řešit ještě:

  • Aplikační, infrastrukturní, doménové servisy
  • Doménové a infrastrukturní eventy
  • Anti-corruption layer
  • Specification pattern
  • Factories

Týmy pracující s DDD…

Přidaná komplexita je ještě horší, jelikož každý člen týmu musí znát DDD – není možné, aby někdo nevěděl, co je to aggregate, proč a jak řeší invarianty. Bez těchto a dalších informací nemůže programátor efektivně pracovat a implementovat DDD.

Jinak řečeno, nestačí, aby jeden člen týmu nastudoval DDD a vyřešili všechny problémy, na které narazí. Nutností je, aby každý člen týmu (včetně juniorních programátorů a nováčků přicházejících do týmu) nastudoval DDD.

Osobně jsem viděl aplikace psané pomocí DDD, na kterých se v průběhu let obměnil celý tým programátorů a znalosti DDD se ztratily. Výsledkem byl tým, který měl kód plný agregátů, value objectů, repozitářů a dalších věcí, ale neměl vůbec tušení, proč tyto věci používají a k čemu slouží.

Hledání dostatečně složité domény

Teď už doufám alespoň trochu pociťujete, kolik práce vám použití DDD přidá. Otázkou je nyní, proč je tato složitost potřeba?

Většina kurzů, knih, architektů a konzultantů vám řekne: “Ano, DDD přidává spoustu komplikací, ale pro složité domény se vyplatí”. Argument složité domény se často opakuje jako mantra bez hlubšího zamyšlení. Zkusme se tedy podívat na data a zjistit, zda opravdu existuje doména natolik složitá, aby použití taktického DDD ušetřilo práci.

Extrémně složité domény

U strategické části DDD můžeme poměrně jednoduše ukázat na většinu velkých firem a říct, že všechny dospěly do situace, kdy musely systém nějakým způsobem rozdělit do menších částí (domén) a DDD jim pak mohlo pomoci toto rozdělení provést.

Můžeme ale něco podobného říct i o taktické části DDD? Například: “Existuje doména natolik složitá, že je v ní použití DDD nezbytné, abyste vůbec dokázali konkurovat aplikacím, které jej využívají?” nebo “Existuje doména natolik složitá, že bez použití DDD je vývoj měřitelně pomalejší oproti konkurenci využívající DDD?”

Osobně jsem nikdy o žádné takové doméně neslyšel.

Použití ve velkých firmách po 25 letech od vzniku DDD

Můžeme se také ptát: “Jak moc o taktickou část DDD používají velké firmy?”. Kdy naposledy jste četli článek od FAANG firmy o úspěchu díky taktickým patternům DDD?

Já jsem osobně neslyšel ani jednu zmínku v posledních X letech. Naopak o strategické části můžeme najít spoustu materiálů. Příkladem může být Uber a jejich DOMA architektura nebo Data Mesh, což je v podstatě aplikace strategického DDD na datovou architekturu.

Použití v Open Source projektech

Dále můžeme hledat použití v Open Source projektech: “Kolik velkých a úspěšných open source projektů, které používají DDD, znáte?” V tomto případě věřím (doufám), že nějaké existují, ale osobně neznám ani jeden.

DDD v dnešní době

Podle informací uvedených výše se zdá, že taktická část DDD se v praxi spíše neosvědčila a dostatečně složitá doména pro jeho použití možná ani neexistuje.

Můžeme se ale inspirovat originálními koncepty a použít je v moderních aplikacích, pokud to dává smysl. Měli bychom však upustit od dogmatické implementace celého taktického DDD, která je stále hojně prezentována a vyučována.

Edit: Event Sourcing

První čtenáři článku vznesli zajímavou otázku: “Pokud pracuji na aplikaci, pro kterou je Event Sourcing + CQRS vhodným řešením, vyplatí se použít i taktické DDD?”.

Osobně nemám mnoho zkušeností s takovými aplikacemi, takže nemůžu odpovědět s jistotou. Můžeme se ale podívat na problémy zmíněné v tomto článku a zkusit si říct, jak je Event Sourcing ovlivní:

  • Problém 1: Mapování – Tento problém je prakticky úplně vyřešený, jelikož aplikace stejně musí používat Event Sourcing a CQRS.
  • Problém 2: Načítání velkých agregátů – Tento problém se nemění, pouze komplikuje, jelikož nemůžete použít lazy loading.
  • Problém 3: Rozdělení agregátů – Nic se nemění.
  • Problém 4: Validace a invarianty – Nic se nemění.
  • Problém 5: Always valid domain model – Problém je ještě horší, jelikož musíte počítat se starými eventy.

Jak můžete vidět, Event Sourcing něco málo vyřeší, ale nezdá se, že by přinesl zásadní změnu.