Kontext
V Mews jsem připravil návrh, jak by se daly hranice Billing domény v monolitu postupně zpřehlednit. Nebyl to hotový přepis systému do samostatné služby, spíš návrh cesty, jak se k jasnějším hranicím postupně dostávat.
Hodnota toho návrhu byla hlavně v analýze: pochopit, proč je Billing těžké oddělit, kde jsou skryté závislosti a co by bylo potřeba změnit, aby hranice domény začaly být viditelnější.
Problém
Problém nebyl jen ve velikosti kódu. Billing byl provázaný se zbytkem systému přes sdílené entity, Entity Framework loadery, navigation properties, společný kontext a další implicitní závislosti.
Stačilo upravit loader v jedné privátní metodě a aplikace mohla začít padat úplně jinde — v části systému, která s tou změnou na první pohled nesouvisela, ale pracovala se stejnými entitami a tiše spoléhala na to, že už někdo načetl potřebné vazby.
Další problém byl v tom, že doménová logika pracovala přímo s databázovým modelem. Entity Framework entity se používaly v business vrstvě, takže hranice mezi persistencí a doménou byla rozmazaná. Nebylo jednoduché doménu zúžit, skrýt některé vlastnosti nebo použít stejnou logiku nad jiným zdrojem dat. Pokud databázová entita nějakou vlastnost vyžadovala, promítalo se to i do míst, kde by ji business logika ideálně vůbec nemusela vidět.
U monolitu se k tomu přidávalo i vlastnictví napříč týmy a doménami. Například rezervační doména, billingová doména a účetnictví mohly pracovat se stejnými položkami objednávky, ale každá doména je potřebovala chápat trochu jinak.
Pro rezervace mohla položka představovat to, co si host objednal. Pro billing byla důležitá tím, jak se účtuje, rozděluje mezi účty nebo dostává na fakturu. Pro účetnictví nad ní mohl vznikat ještě jiný pohled související s cenou, změnami nebo finanční správností.
Vedlejší efekt byl, že některé funkce se kvůli tomu nedaly dobře navrhnout. Jedna položka musela zároveň splňovat představu rezervací, billingu i účetnictví. Jakmile by nová funkce pomohla jedné doméně, mohla začít porušovat pravidla jiné.
I zdánlivě jednoduché věci, jako změna ceny nebo rozdělení položky, pak nebyly jednoduché. Nešlo jen o úpravu jedné hodnoty. Bylo potřeba řešit, co tím vznikne z pohledu účtování, fakturace, účetnictví a historie dat.
Výsledkem bylo, že Billing neměl vlastní jasně ohraničený model. Pracoval s objekty, které zároveň nesly očekávání dalších domén.
Navržené řešení
Návrh nesměřoval k tomu, aby se Billing okamžitě vyřízl do nové služby. V takovém systému by to bylo příliš rizikové. Dávalo mi větší smysl nejdřív vytvořit jasnější hranici přímo uvnitř monolitu.
Hlavní myšlenka byla přestat pouštět billingovou logiku přímo k celému sdílenému grafu entit. Místo toho by měla pracovat s užšími kontrakty, které říkají, jaká data dané pravidlo opravdu potřebuje.
Proto jsem navrhl použít interface pro ořezání velkých entit a jejich navigation properties. Nebyl to finální cílový model, ale přechodový krok: business logika by dál mohla dostat data z existujících Entity Framework entit, ale už by neviděla všechno, co tyto entity obsahují.
Další krok měl být nahradit implicitní loadery ručním načtením a mapováním dat pro konkrétní flow. Místo univerzálního objektového grafu, který se může donekonečna rozšiřovat přes navigation properties, by konkrétní use case dostal vlastní strom dat: například pohled na položky z hlediska billingu nebo pohled na položky z hlediska rezervace.
Tím by se oddělilo, co pro daný use case znamená „položka“. Billingová logika by nemusela pracovat s obecnou sdílenou entitou, která zároveň nese očekávání rezervací i účetnictví. Dostala by model, kde je jasné, které vazby jsou načtené, které vlastnosti jsou potřeba a kde strom končí.
Součástí návrhu byla také Billing module facade. Smyslem nebylo schovat složitost za jednu velkou metodu, ale dát ostatním částem systému jasnější vstupní bod do billingové logiky. Pokud okolní kód přestane sahat přímo na vnitřní entity a služby Billingu, začne být lépe vidět, co je veřejný kontrakt domény a co je implementační detail.
Výsledek
Návrh se nakonec nedostal do plné realizace, ale pro mě to byla jedna z nejucelenějších architektonických prací, které jsem v Mews připravil. Nebyl to jen nápad „oddělme Billing“. Byl to pokus popsat konkrétní cestu, jak se z provázaného grafu entit dostat k jasnějším doménovým hranicím bez velkého přepisu systému.
Nejcennější na tom pro mě bylo, že návrh nebyl použitelný jen pro Billing. Billing byl konkrétní doména, na které se problém dobře ukazoval, ale stejný princip by šel aplikovat i jinde v monolitu: zúžit vstupy, pojmenovat kontrakty, nahradit implicitní loadery explicitním mapováním a postupně měnit obecný graf entit na menší modely podle konkrétních use casů.
Právě v tom mi ten návrh připadal silný. Neřešil jen jednu třídu nebo jednu službu, ale způsob, jak postupně měnit tvar velkého existujícího systému bez toho, aby bylo potřeba všechno přepsat najednou.
Detaily popisu záměrně držím obecné, protože nejdůležitější pro mě není interní implementační plán, ale přenositelný princip práce s velkým existujícím systémem.