Kapitola 18 · Praxe · Migrace z CRUD architektury na DDD

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.

Autor M. Katuščák
Doba čtení ≈ 25 min
Náročnost pokročilá
Publikováno · Aktualizováno ·
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í.

FIG. 18.2-A Strangler Fig: čtyři fáze migrace CRUD → DDD

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 OrderService obsahuje 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ě.

  1. Identifikujte invarianty entity (co nesmí být porušeno).
  2. Pro každý invariant najděte metodu v *Service, která ho dnes drží.
  3. Přesuňte metodu do entity, getter/setter zúžte na private nebo zrušte.
  4. Service se stane tenkým koordinátorem (Application Service) – jen volá entitu, transakce, eventy.
  5. 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#[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:

  1. Zaveďte Persisted Object Pattern – doménová třída zůstane POPO, persistence model + mapper jdou do App\<BC>\Infrastructure\Persistence\Doctrine\.
  2. Mapper hydratujte z perzistence přes User::reconstitute(...) factory metodu, která neemituje doménové události.
  3. Hlídejte hranici staticky: composer require --dev phpat/phpat + rule App\<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.

  1. Zaveďte VO OrderId (final readonly class OrderId { public function __construct(public Uuid $value) {} }, generování přes Uuid::v7()).
  2. Doctrine custom type pro OrderId (mapping z DB string ↔ VO).
  3. 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í.

  1. Identifikujte vlastnícího BC (typicky Identity).
  2. Ostatní BC do ní nesmí zapisovat – jen číst. Čtení přesuňte do read-modelů (každý BC má vlastní projekci).
  3. Zápisy nahraďte voláním Identity API (sync HTTP nebo async event publishing s outboxem).
  4. Souvisí: Outbox Pattern.

Recept 5: Doménová logika v controlleru

Symptomy: 200řádkový controller s if-else stromem doménových rozhodnutí.

  1. Vytvořte Command DTO + CommandHandler v Application vrstvě.
  2. Controller se zúží na: validate input → dispatch command → vrátit response.
  3. Autorizaci přesuňte do Voteru (souvisí Autorizace).

Recept 6: Aggregate bobtná (1000+ řádků)

Symptomy: Order má 30 metod a 15 polí.

  1. Najděte pole, která se mění nezávisle (různé invarianty, různé use cases).
  2. Zvažte rozdělení na 2 agregáty (např. Order + OrderShipment). Spojí je sdílené OrderId, žádná silná reference.
  3. Specifikační logiku vyextrahujte do Specification tříd (souvisí Specifications).

Recept 7: eventDispatcher->dispatch() uvnitř doménové metody

Symptomy: Aggregate volá Symfony EventDispatcher přímo.

  1. Aggregate dědí z AggregateRoot a eventy zaznamenává voláním record($event).
  2. Aplikační handler po repository->save() volá $order->releaseEvents() a publikuje (přes outbox).
  3. Doména ztratí závislost na Symfony EventDispatcheru. Test je čistý.
  4. Souvisí: Outbox – Aggregate publikuje.

Recept 8: Stav je sloupec string $status

Symptomy: Order::$status: string, podmínky všude if ($order->status === 'PLACED').

  1. Zaveďte enum (PHP 8.1+): enum OrderStatus: string { case PLACED = 'placed'; case CANCELLED = 'cancelled'; }.
  2. Aggregate metody dělají transitions: $this->status = OrderStatus::CANCELLED.
  3. 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í.