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.
- Tým se učí 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.
Datová migrace při Strangler Fig
Kód se dá nahrazovat po částech, data ne – tabulka má v každém okamžiku jeden tvar. Strangler Fig proto potřebuje plán, jak data převést do nového modelu bez výpadku a s možností návratu. Osvědčený postup má čtyři fáze.
1. Dual-write s porovnáním. Aplikace začne zapisovat do starého i nového modelu současně. Primární zůstává starý zápis; ten nový se provádí navíc a jeho chyba nesmí shodit požadavek. Asynchronní job oba zdroje porovnává a rozdíly loguje. Každý nalezený rozdíl znamená chybu v mapování, kterou je nutné opravit ještě před přepnutím.
2. Backfill. Teprve po zapnutí dual-write naplní jednorázový skript nové tabulky historickými
daty. Obrácené pořadí je vadné: UPDATE legacy řádku, který backfill už zpracoval, by se před
zapnutím dual-write ztratil – checkpoint WHERE id > checkpoint ho podruhé nenačte. Skript musí
být idempotentní: opakované spuštění nesmí vytvořit duplicity ani přepsat novější záznam, který
mezitím zapsal dual-write. Běží po dávkách podle id nebo updated_at, při konfliktu vyhrává
novější záznam, a ukládá si checkpoint posledního zpracovaného řádku, takže po pádu naváže tam,
kde skončil.
3. Shadow reads. Čtení probíhá z obou zdrojů: odpověď uživateli sestavuje starý model, výsledek toho nového se pouze porovná a neshoda zvedne alert. Teprve nulová míra rozdílů po dnech až týdnech provozu dává jistotu, že nový model je úplný a správný.
4. Cutover. Přepnutí na nový model řídí feature flag, ne deploy. Provoz se převádí postupně – 1 %, 10 %, 50 %, vše – a metriky z fáze shadow reads zůstávají zapnuté. Rollback znamená přepnout flag zpět; starý model je díky dual-write stále aktuální. To platí jen tehdy, když po přepnutí primáru dual-write pokračuje v obráceném směru – nový model zapisuje zpět do starého. Starý zápis se vypíná jako úplně poslední krok, po několika týdnech klidného provozu.
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 zapojuje do návrhu i lidi mimo tým vývoje. Při migraci z CRUD pomáhá odkrýt implicitní doménovou logiku skrytou v kontrolerech a service třídách, identifikovat přechody stavů entit (z pohledu domény, nikoli databáze), najít přirozené hranice Bounded Contexts a přizvat doménové experty k 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 objektů, které je vlastní. Cíl: tyto 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ž poté, 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 natrvalo provozovat 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.
Při extrakci logiky z legacy kódu pomáhají i jazykové modely: vygenerují první sadu charakterizačních testů nebo popíší, co nepřehledná metoda dělá. Souvislosti tohoto přístupu přibližuje kapitola DDD a umělá inteligence.
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ý.
- ORM diktující tvar modelu – Anti-vzorem není atributové mapování samo o sobě; sekce 18.04 i Recept 2 ho přijímají jako pragmatickou volbu. Problém začíná, když ORM určuje tvar modelu: public settery kvůli hydrataci, anemická entita,
flush()volaný z kontroleru. Projekty, které potřebují striktní oddělení domény od persistence, řeší tutéž potřebu přes Persisted Object Pattern. - 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 Uuid $value) {} }, generování přesUuid::v7()). - 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 dědí z
AggregateRoota eventy zaznamenává volánímrecord($event). - 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í.