Migrace z CRUD architektury na DDD
Podrobný průvodce migrací z CRUD architektury na Domain-Driven Design v Symfony. Strangler Fig Pattern, extrakce doménové vrstvy, zavedení repozitářů a postupné zavedení CQRS s praktickými PHP příklady.
Obsah kapitoly
18.01 Kdy a proč migrovat z CRUD na DDD
CRUD architektura (Create, Read, Update, Delete) je výchozí volba pro většinu aplikací a dlouho stačí. Pro správu dat bez komplexní logiky – záznamy kontaktů, katalogy produktů, administrační rozhraní – CRUD odvede práci a vrstvy DDD by byly zbytečnou zátěží. Problém přijde, když aplikace přeroste do větší komplexity a doménová logika proniká na nevhodná místa.
Kdy DDD přináší hodnotu a kdy je CRUD dostačující
Rozhodnutí o migraci stojí na analýze komplexity domény, ne na trendech. Martin Fowler ve své práci o architektonických vzorech ukazuje, že Transaction Script a CRUD jsou legitimní volbou pro aplikace s jednoduchými doménovými pravidly [1].
Realistické zhodnocení nákladů migrace
Migrace z CRUD na DDD trvá měsíce až roky podle velikosti kódové základny — jde o dlouhý proces, ne jednorázovou akci. Sama o sobě nepřináší zákazníkovi okamžitou hodnotu – hodnota přijde s tím, jak tým začne přidávat funkce rychleji a s menším rizikem regresí. Management přijme migraci snáz, když probíhá inkrementálně souběžně s vývojem nových funkcí, ne jako izolovaný refaktoringový projekt.
18.02 Strangler Fig Pattern – vzor postupné náhrady
Strangler Fig Pattern (vzor fíkovníku škrtiče) pojmenoval Martin Fowler [2]. Vzor nahrazuje starý systém po částech, bez „big bang“ přepisu. Název pochází od tropického fíkovníku, který roste kolem hostitelského stromu a postupně ho zardousí.
Výhody oproti přímé refaktorizaci (Big Bang Rewrite)
Přepsat celý systém najednou (tzv. „big bang rewrite“) je jedno z největších rizik v softwarovém vývoji. Joel Spolsky ve svém článku „Things You Should Never Do“ [3] popisuje, proč firmy ztratily konkurenční výhodu tím, že kompletně přepsaly fungující systémy. Strangler Fig Pattern oproti tomu:
- Umožňuje kontinuální dodávku nové hodnoty zákazníkovi i během migrace.
- Snižuje riziko – systém nikdy není kompletně „rozbitý“.
- Poskytuje možnost rollbacku: pokud nová implementace selhává, stará stále funguje.
- Umožňuje týmu učit se DDD postupně, na reálném produkčním kódu.
- Refaktoring lze zastavit kdykoli – systém zůstává v konzistentním, funkčním stavu.
18.03 Krok 1: Analýza existující domény
Než začneme přesouvat kód, musíme pochopit doménu. Nejčastější chybou je přímý skok do refaktoringu bez předchozí analýzy – výsledkem je pak DDD architektura, která přesně kopíruje strukturu starých databázových tabulek, aniž by odrážela skutečný doménový model.
Identifikace Bounded Contexts z existujícího CRUD kódu
Bounded Contexts lze v existující CRUD aplikaci identifikovat sledováním přirozených hranic:
- Skupiny entit a tabulek, které jsou silně provázané navzájem, ale slabě propojené s ostatními skupinami – to jsou kandidáti na jeden Bounded Context.
- God Services – velké service třídy jsou paradoxně dobrým vodítkem. Pokud
OrderServiceobsahuje logiku objednávky, platby i doručení, jsou to tři různé Bounded Contexts skryté v jedné třídě. - Opakující se slovo s různým významem – pokud „zákazník“ v kontextu prodeje znamená něco jiného než „zákazník“ v kontextu zákaznické podpory, jde o přirozené rozhraní dvou Bounded Contexts.
Event Storming jako nástroj pro analýzu
Event Storming vymyslel Alberto Brandolini [4]. Workshopová technika modeluje doménu přes doménové události a zapojí do návrhu i lidi mimo tým vývoje. Při migraci z CRUD pomáhá:
- Odkrytí implicitní doménové logiky skryté v kontrolerech a service třídách.
- Identifikaci přechodů stavů entit (z pohledu domény, nikoli databáze).
- Nalezení přirozených hranic Bounded Contexts.
- Zapojení doménových expertů do návrhu nové architektury.
18.04 Krok 2: Extrakce doménové vrstvy
Extrakce doménové vrstvy přesouvá doménová pravidla z kontrolerů a service tříd do doménových objektů. Cíl: doménové objekty si své invarianty hlídají samy. Nikdo zvenčí je nemůže obejít.
Přesunutí doménových pravidel do doménových objektů
Refaktoring má dva kroky. Nejdřív vzniknou Value Objects pro primitivy s doménovými pravidly. Pak se logika přesune do entit a doménových služeb.
Zavedení Value Objects místo primitive types
Doménový koncept skrytý v string nebo int se nazývá Primitive Obsession. Value Object nahradí
primitiv objektem, který drží validaci i chování pohromadě.
18.05 Krok 3: Zavedení repozitářů
CRUD aplikace typicky volá EntityManagerInterface nebo Doctrine repozitáře přímo z kontrolerů
a service tříd. DDD postaví mezi doménu a persistenci doménové rozhraní repozitáře. Doménový kód
o Doctrine ani SQL nic neví a implementace se dá vyměnit bez jeho úprav.
Vytvoření doménového rozhraní repozitáře
Doménové rozhraní repozitáře žije v doménové vrstvě. Popisuje operace tak, jak je potřebuje doména. O Doctrine, SQL ani jiné infrastruktuře nepadne ani zmínka.
18.06 Krok 4: Postupné zavedení CQRS
Command Query Responsibility Segregation (CQRS) na DDD navazuje, ale má se zavést až po tom, co se doménový model usadí. Když přijde dřív, přesune komplexitu z domény do handleru, kde je neviditelná a hůř se testuje.
Začít s Command stranou (write side)
Nejpřirozenějším místem pro zavedení CQRS je write side – operace, které mění stav systému. Query side (čtení) lze zpočátku ponechat s přímými Doctrine dotazy a refaktorovat ji samostatně, nebo ji ponechat jako optimalizované SQL dotazy i v DDD systému (read modely).
18.07 Testování při migraci
O úspěchu migrace rozhodují testy. Bez nich refaktoring zavede regrese, které se projeví v produkci. Migrace z CRUD na DDD potřebuje dvě techniky: charakterizační testy pro zachycení stávajícího chování a unit testy pro nově vznikající doménovou vrstvu.
Charakterizační testy (Characterization Tests)
Pojem „charakterizační testy“ pochází z knihy Michaela Featherse „Working Effectively with Legacy Code“ [5]. Charakterizační test nepopisuje, jaké by mělo být správné chování systému, ale zachycuje jaké chování systém aktuálně má. Slouží jako síť, která zachytí nechtěné změny chování při refaktoringu.
Unit testy doménové vrstvy
Jednou z výhod DDD je testovatelnost doménových objektů v izolaci bez databáze, HTTP klienta nebo jiné infrastruktury. Unit testy doménové vrstvy jsou rychlé, deterministické a přesně dokumentují doménová pravidla.
18.08 Rizika a doporučení
Nejčastější chyby při migraci
- Anémický doménový model – Nejčastější past. Vývojáři vytvoří třídy s názvem jako v DDD (
User,Order), ale tyto třídy obsahují pouze gettery a settery bez doménové logiky. Logika zůstane v service třídách. Výsledek je DDD terminologie s CRUD implementací. - Přílišná granularita Bounded Contexts – Rozdělení domény na příliš mnoho malých kontextů vede k distribuované komplexitě. Každá integrace mezi kontexty přidává overhead. Začněte s většími kontexty a rozdělujte je až tehdy, když je důvod k tomu jasný.
- Doctrine entity jako doménové entity – Přímé přidávání DDD logiky do Doctrine entit je anti-vzor. Doctrine mapování (anotace, atributy) svazuje doménový objekt s infrastrukturní technologií. Oddělte doménové entity od persistence mapování.
- CQRS bez doménového modelu – Zavedení CommandBusu a QueryBusu bez refaktorovaného doménového modelu přidá vrstvy komplexity bez přínosu. CQRS je amplifikátor – zesílí jak výhody, tak problémy stávající architektury.
- Ignorování Anti-Corruption Layer – Při integraci nové DDD vrstvy se starým CRUD kódem je nutné vytvořit překladovou vrstvu. Bez ní pronikají koncepty starého modelu do nového a kontaminují ho.
Tipy pro týmovou komunikaci
- Vytvořte glosář pojmů (Ubiquitous Language) a udržujte ho aktuální. Vyvěste ho na wiki nebo přímo v repozitáři jako součást dokumentace.
- Pravidelně pořádejte krátká Event Storming sezení (30–60 minut) pro nové funkcionality před jejich implementací.
- Nastavte code review pravidla: doménová logika nesmí být v kontrolerech, doménové objekty nesmějí záviset na infrastruktuře.
- Komunikujte s managementem v pojmech obchodní hodnoty, nikoli technické architektury. Migrace na DDD = schopnost rychleji a bezpečněji přidávat nové funkce.
Realistické odhady náročnosti
Inkrementální migrace středně velké CRUD aplikace (50–100 tabulek, 3–5 let vývoje) na DDD trvá v praxi 12 až 24 měsíců. Číslo počítá s tím, že migrace běží souběžně s vývojem nových funkcí a nemá dedikovaný tým na plný úvazek. Co dobu prodlužuje: špatná testovatelnost stávajícího kódu (nutnost psát charakterizační testy), slabá znalost domény v týmu, chybějící doménoví experti.
DDD koncepty a jejich implementaci v Symfony rozebírají navazující kapitoly Implementace DDD v Symfony a CQRS v Symfony.
18.09 Refaktoring kuchařka – krátké recepty
Strangler Fig je strategický pohled na celou migraci. V denní praxi narazíte na opakující se mikrosituace. Tato kuchařka obsahuje 8 nejčastějších, každá ve formátu „symptomy → krok 1, 2, 3“. Recepty jsou záměrně krátké – když potřebujete kontext nebo důkladnější rozbor, projděte odkazované kapitoly.
Recept 1: Anémická Doctrine entita
Symptomy: entita má jen gettery/settery, veškerá logika je v Service třídě.
- Identifikujte invarianty entity (co nesmí být porušeno).
- Pro každý invariant najděte metodu v
*Service, která ho dnes drží. - Přesuňte metodu do entity, getter/setter zúžte na
privatenebo zrušte. - Service se stane tenkým koordinátorem (Application Service) – jen volá entitu, transakce, eventy.
- Souvisí: Anti-vzor: Anemic Domain Model · Domain Services vs. Application Services.
Recept 2: Doctrine atributy v doménové třídě – kdy je to problém
Symptomy: App\Domain\Order má #[ORM\Entity], doména závisí na Doctrine.
Pragmatická výchozí volba v tomto průvodci atributy přijímá – jsou to metadata, ne chování, a Symfony ekosystém s nimi pracuje idiomaticky (viz rozhodnutí o mappingu). Pokud váš projekt skutečně potřebuje striktní oddělení (Hexagonal, dlouhodobá výměna ORM, core doména s vysokou hodnotou), postup je:
- Zaveďte Persisted Object Pattern –
doménová třída zůstane POPO, persistence model + mapper jdou do
App\<BC>\Infrastructure\Persistence\Doctrine\. - Mapper hydratujte z perzistence přes
User::reconstitute(...)factory metodu, která neemituje doménové události. - Hlídejte hranici staticky:
composer require --dev phpat/phpat+ ruleApp\<BC>\Domain\* nesmí závisět na Doctrine\*.
Recept 3: Primitivní ID jako string / int
Symptomy: Order::$id: string, kdekoli se předává jen string.
- Zaveďte VO
OrderId(final readonly class OrderId { public function __construct(public Ulid $value) {} }). - Doctrine custom type pro
OrderId(mapping z DB string ↔ VO). - Postupně refaktorujte signature napříč handlery. PHPStan na úrovni 8 odhalí každý zapomenutý
string.
Recept 4: Doctrine tabulka sdílená napříč BC
Symptomy: tabulka users se používá v Ordering BC i Billing BC; oba do ní zapisují.
- Identifikujte vlastnícího BC (typicky Identity).
- Ostatní BC do ní nesmí zapisovat – jen číst. Čtení přesuňte do read-modelů (každý BC má vlastní projekci).
- Zápisy nahraďte voláním Identity API (sync HTTP nebo async event publishing s outboxem).
- Souvisí: Outbox Pattern.
Recept 5: Doménová logika v controlleru
Symptomy: 200řádkový controller s if-else stromem doménových rozhodnutí.
- Vytvořte
CommandDTO +CommandHandlerv Application vrstvě. - Controller se zúží na: validate input → dispatch command → vrátit response.
- Autorizaci přesuňte do Voteru (souvisí Autorizace).
Recept 6: Aggregate bobtná (1000+ řádků)
Symptomy: Order má 30 metod a 15 polí.
- Najděte pole, která se mění nezávisle (různé invarianty, různé use cases).
- Zvažte rozdělení na 2 agregáty (např.
Order+OrderShipment). Spojí je sdílenéOrderId, žádná silná reference. - Specifikační logiku vyextrahujte do
Specificationtříd (souvisí Specifications).
Recept 7: eventDispatcher->dispatch() uvnitř doménové metody
Symptomy: Aggregate volá Symfony EventDispatcher přímo.
- Aggregate uchová eventy v
private array $releasedEvents. - Aplikační handler po
repository->save()volá$order->releaseEvents()a publikuje (přes outbox). - Doména ztratí závislost na Symfony EventDispatcheru. Test je čistý.
- Souvisí: Outbox – Aggregate publikuje.
Recept 8: Stav je sloupec string $status
Symptomy: Order::$status: string, podmínky všude if ($order->status === 'PLACED').
- Zaveďte enum (PHP 8.1+):
enum OrderStatus: string { case PLACED = 'placed'; case CANCELLED = 'cancelled'; }. - Aggregate metody dělají transitions:
$this->status = OrderStatus::CANCELLED. - Pro komplexní transition rules zvažte State Machine (Symfony Workflow component nebo doménová reprezentace).
Časté otázky
Jaké příznaky ukazují, že CRUD aplikace je zralá na migraci?
Typickými signály jsou God Services o stovkách řádků a kontrolery obsahující doménová pravidla. Dále doménová logika zamíchaná v Doctrine repozitářích, opakované regresní chyby při drobných změnách a rostoucí čas potřebný pro onboarding nových vývojářů. Pokud aplikace tyto příznaky nevykazuje a zůstává prostým mapováním formulářů na tabulky, migrace odpovídající hodnotu nepřinese. Obecnější otázku, pro jaké projekty je DDD vhodné, řeší samostatná kapitola Kdy DDD nepoužívat. Viz také sekci Kdy a proč migrovat.
Co je Strangler Fig Pattern?
Strangler Fig (fíkovník škrtič) je migrační vzor popsaný Martinem Fowlerem, při kterém nová architektura postupně „obroste“ starý systém a nahradí ho po částech. Nová funkcionalita vzniká od začátku v DDD stylu, zatímco stará CRUD část zůstává v provozu a s každou iterací ubývá. Obě části existují paralelně a propojují se přes Anti-Corruption Layer. Podrobný rozbor v sekci Strangler Fig Pattern.
Jak začít s analýzou existující domény?
Začíná se Event Stormingem nebo obdobnou kolaborativní technikou s doménovými experty – zmapují se hlavní události, commands a aktéři. Z této mapy vyplývá návrh Bounded Contexts a Ubiquitous Language. Paralelně se v existujícím kódu hledají implicitní hranice modelu: moduly, tabulky nebo funkční celky, které jsou málo propojené. Cílem první iterace je hrubá mapa, ne úplný model. Praktický postup v sekci Analýza existující domény.
Jak extrahovat doménovou vrstvu z existujícího CRUD kódu?
Migrace začíná u jednoho vybraného Bounded Contextu, pro který vzniká nová doménová vrstva oddělená od Doctrine entit. Doménová logika ze service tříd a kontrolerů se přesouvá do metod agregátu, zatímco původní CRUD kód zůstává jako adaptér pro API a persistenci. Nejprve se zavede Anti-Corruption Layer, pak se refaktorují jednotlivé use casy. Charakterizační testy proti původnímu chování minimalizují regrese. Detailní rozbor v sekci Extrakce doménové vrstvy.
Jaká jsou hlavní rizika migrace z CRUD na DDD a jak je zmírnit?
Nejčastější pastí je anémický model: nové třídy mají DDD názvy, ale logika zůstává v servisech. Dále hrozí nadměrná granularita Bounded Contexts, přímé ukládání doménové logiky do Doctrine entit a zavádění CQRS bez přepracovaného modelu. Největším rizikem je Big Bang Rewrite, který se zřídka dotáhne do konce. Migrace má probíhat inkrementálně přes Strangler Fig, u středně velké aplikace s realistickým odhadem 12–24 měsíců. Rozbor rizik a zmírňujících opatření v sekci Rizika a doporučení.