Tahle kapitola není o tom, co tvůj backend dělá, ale jak je poskládaný uvnitř — kam který kus kódu patří. Většina backendů totiž neumře na špatný algoritmus, ale na chaos: pravidla appky rozházená všude možně, jedna malá změna rozbije pět nesouvisejících věcí, a po půl roce se v tom nikdo nevyzná. Dobrá architektura tomu předchází. Vezmeme to od základních pojmů.
Pár pojmů na úvod
- Byznys logika = pravidla tvojí aplikace, ta „chytrá" část. Např. „uživatel nesmí mít víc než 3 aktivní objednávky" nebo „slevu lze uplatnit jen jednou". To je srdce appky.
- Controller (route handler) = kód, který přijme příchozí HTTP request a rozhodne, co s ním.
- Service = kód, kde žije byznys logika.
- Repository = kód, který se stará o ukládání a čtení z databáze.
- ORM = knihovna, přes kterou pracuješ s databází pomocí objektů místo psaní SQL.
- Závislost (dependency) = něco, co jeden kus kódu potřebuje k práci (např. service potřebuje repository, aby se dostal k datům).
Nelekej se, projdeme to teď na jednoduchém příměru.
Vrstvy — jako restaurace
Nejjednodušší způsob, jak backend uspořádat, je rozdělit ho do vrstev, kde každá má jednu práci. Představ si restauraci:
- Controller = číšník. Převezme od hosta požadavek, zkontroluje, že dává smysl, předá ho do kuchyně a pak donese výsledek. Sám nevaří — žádná byznys logika, žádné SQL.
- Service = kuchař. Tady se odehrává ta „chytrá" práce podle pravidel (ověř, že má uživatel místo na další objednávku, spočítej cenu se slevou). Neřeší, jak přišel požadavek (HTTP?), ani kde jsou data uložená.
- Repository = spíž. Umí jen donést a uložit data („dej mi uživatele s ID 5"). Service neřeší, jestli je za tím Postgres, nebo něco jiného.
Pravidlo, které si zapamatuj: tencí číšníci, vytížení kuchaři, hloupá spíž (thin controllers, fat services, dumb repositories). Když začneš vařit u stolu (byznys logika v kontroleru), nejde to pak znovupoužít odjinud (třeba ze zpracování fronty) ani pořádně otestovat.
Separation of Concerns — každý dělá jen svoje
Hlavní myšlenka výše má jméno: separation of concerns (oddělení starostí). Každý kus kódu má řešit jednu věc. Když jedna funkce zároveň parsuje HTTP, počítá cenu, posílá e-mail a píše do databáze, nejde ji bezpečně změnit — sáhneš na jedno a rozbiješ druhé.
Dva pojmy, které k tomu patří:
- Cohesion (soudržnost) — věci, co spolu souvisí, jsou pohromadě. Chceš ji vysokou.
- Coupling (provázanost) — jak moc jeden modul závisí na druhém. Chceš ho nízký.
Cíl: vysoká soudržnost, nízká provázanost. Každý modul dělá jednu věc pořádně a o ostatních ví co nejmíň.
Hexagonal architecture — když to chceš zatáhnout dál
Jakmile appka roste, používá se pokročilejší uspořádání (uvidíš pojmy hexagonal architecture, ports & adapters, clean architecture — je to v zásadě totéž). Myšlenka: byznys logika (doména) je uprostřed a neví vůbec nic o vnějším světě. Všechno vnější — HTTP, databáze, fronta, e-mail — jsou adaptéry, které se připojují přes porty (rozhraní = domluvený „zásuvkový" tvar).
Klíčové pravidlo (dependency rule): závislosti směřují dovnitř, k doméně. Doména nezávisí na databázi; je to naopak — databáze se přizpůsobí doméně. Výhoda: vyměníš databázi za jinou, nebo HTTP za zpracování fronty, a jádro appky se nezmění. A jádro se dá otestovat úplně bez databáze.
Ale pozor na opačný extrém. Pro malou appku jsou plné vrstvy a porty zbytečná ceremonie. Začni jednoduše (klidně controller → service → ORM) a přidávej strukturu, až když složitost reálně roste. Architektura ti má sloužit, ne být náboženství.
Kam patří byznys logika
Nejčastější chyba začátečníka: napsat pravidla appky do kontroleru nebo přímo do databázových modelů. Patří do service / doménového modelu. Dva přístupy, jak to uspořádat:
- Anemic model — entity jsou jen „pytlíky na data" bez chování a všechna logika je v service. Jednoduché a běžné, ale logika se může rozlézt do moc míst.
- Rich domain model — entita si sama hlídá svoje pravidla (
objednavka.zrus()ověří, že se zrušit dá). Logika je u dat, hůř se rozteče. Je to srdce DDD (viz níže).
Ať zvolíš cokoli: byznys logika NIKDY nepatří do kontroleru ani do databázové vrstvy.
DDD-lite — pár užitečných pojmů
DDD (Domain-Driven Design) je celá filozofie návrhu kolem domény. Nemusíš ji dělat celou, ale tyhle pojmy se ti budou hodit:
- Entity — věc s identitou, která přetrvává změny (uživatel #42 je pořád ten samý, i když změní jméno).
- Value object — věc definovaná svou hodnotou, neměnná (peníze, adresa).
Money(100, 'CZK'); dvě stejné částky jsou zaměnitelné, nemají vlastní identitu. - Aggregate — skupina entit, které se mění jako celek přes jeden „hlavní" objekt (objednávku
a její položky měníš jen přes
Order). Drží data konzistentní. - Ubiquitous language — kód mluví jazykem byznysu: metoda se jmenuje
rezervujMisto(), neupdateRow(); třída jeFaktura, neDataRow2. Ať kód čte i člověk z byznysu.
A proč jen „lite"? Výše jsou taktické vzory DDD — stavební kameny kódu, co se hodí skoro vždy. Celé DDD má ještě strategickou vrstvu pro návrh ve velkém, když je doména složitá nebo na ní dělá víc týmů:
- Bounded context (ohraničený kontext) — hranice, uvnitř které má každý pojem jeden přesný význam. „Zákazník" ve fakturaci a „Zákazník" v podpoře jsou dva různé modely — nemíchej je do jednoho univerzálního
User.- Subdomény — co je tvoje jádro (konkurenční výhoda, tam investuj čas), co jen podpůrné a co generické (kup/použij hotové — viz Tech Choices).
- Context mapping / anti-corruption layer — jak spolu kontexty mluví a jak se jeden brání tomu, aby mu cizí model prosákl do jeho jazyka (překladová vrstva na hranici).
- Domain events — kontexty se o změnách dozvídají přes události („objednávka zaplacena"), ne sáháním si navzájem do dat (viz Messaging a outbox v Patternech).
Celé DDD je investice, která se vyplatí u složité domény a víc týmů; na obyčejnou CRUD appku je to kanón na vrabce. Proto tady bereš jen ty taktické kousky — a strategické sáhneš, až doména začne bolet.
DTO a mapování mezi vrstvami
Data nemají mít všude stejný tvar. Jinak vypadá entita v databázi (viz výše) a jinak to, co pošleš klientovi přes API. DTO (Data Transfer Object) je prostý objekt bez logiky, jehož jediný úkol je přenést data přes hranici (typicky mezi API a klientem) ve správném tvaru.
Proč nikdy neposílat klientovi přímo databázovou entitu:
- Únik citlivých polí — entita
Usermá ipasswordHash,isAdmin. Když ji pošleš celou, vyzradíš je (bezpečnostní díra, viz Security). - Provázanost — API navázané na databázi se rozbije klientům při každé změně schématu (přejmenuješ sloupec → spadne mobilní appka).
- Jiný tvar — klient často chce data jinak, než leží v databázi.
Proto se na hranici vytvoří DTO: request DTO (co přijímáš — hned ho zvaliduješ a vezmeš jen
povolená pole, aby klient nepodstrčil isAdmin: true) a response DTO (co posíláš — jen pole, co
má vidět). Při průchodu aplikací tak data mění tvar a mapper je mezi tvary převádí:
Pozor i tady na opačný extrém: u triviální appky můžou být tři skoro stejné tvary zbytečná byrokracie. Začni jednoduše a odděluj DTO, až ti chybějící hranice začne dělat problémy (únik polí, rozbíjení API).
Dependency Injection — předávej závislosti zvenčí
Když si nějaká třída sama vyrobí to, co potřebuje (new PostgresRepo()), je s tím napevno
svázaná. Dependency Injection (DI) znamená, že jí to potřebné předáš zvenčí (typicky v
konstruktoru):
// ❌ napevno svázané – v testu to nejde obejít, potřebuješ reálnou databázi
class UserService { repo = new PostgresRepo() }
// ✅ injektované – v testu předáš falešné (fake) repo, žádná databáze
class UserService { constructor(private repo: UserRepository) {} }
Výhody: snáz se testuje (podstrčíš falešnou závislost), snadno vyměníš implementaci a je jasně vidět, na čem třída závisí. Právě DI dělá hexagonální architekturu prakticky použitelnou.
Struktura složek: podle vrstev, nebo podle feature?
| Podle vrstev (by layer) | Podle feature (doporučeno, jak appka roste) | |
|---|---|---|
| Složky | controllers/, services/, repos/ | users/, orders/, billing/ |
| Výhoda | Jasně vidět vrstvy | Vše k jedné funkci pohromadě, snadno smazat/přesunout |
| Nevýhoda | Jedna funkce roztažená po 5 složkách | Vrstvy si musíš hlídat uvnitř každé feature |
Malý projekt: „podle vrstev" stačí. Jak roste, „podle feature" se obvykle udržuje líp — a je to první krok k modulárnímu monolitu (níže).
Modulární monolit — hranice bez distribuovaného pekla
Mezi „jeden velký monolit, kde si všechno sahá na všechno" a „microservices přes síť" je zlatá střední
cesta: modulární monolit. Je to pořád jeden nasaditelný program (jeden deploy, lokální
transakce, žádná síťová režie), ale uvnitř rozdělený na moduly s tvrdými hranicemi — a každý modul
(users, orders, billing) je vlastně ten bounded context z DDD výše.
Co dělá modul modulem (a ne jen složkou):
- Vlastní doména a vlastní data. Modul
billingnesahá přímo do tabulek moduluorders. Když něco potřebuje, poprosí přes veřejné rozhraní druhého modulu, ne přes jeho databázi. - Komunikace jen přes public API modulu — pár funkcí/rozhraní ven, zbytek je skrytý (private). Uvnitř si modul dělá, co chce; navenek slibuje jen to rozhraní (může ho přepsat, aniž rozbije ostatní).
- Žádné importy cizích vnitřností —
ordersnesmíimportovat interní třídu zbilling. Přesně tuhle hranici u obyčejného monolitu nikdo nehlídá, a proto se časem rozteče do „velké koule bláta".
Proč to chtít: dostaneš jasné hranice skoro jako u microservices, ale bez distribuovaného pekla — pořád máš lokální transakce, jeden deploy a snadný debug. A když jednou nějaký modul fakt potřebuješ odříznout do samostatné služby, hranici už máš hotovou: vyřízneš ho, protože nikdy nesahal cizím do dat. Proto je doporučená cesta monolit → modulární monolit → (teprve až to bolí) microservices, ne skok rovnou k microservices (viz Tech Choices).
🚩 Nejčastější podvod: „modulární monolit", kde moduly navzájem
importují vnitřnosti a sdílejí tabulky. To je jen obyčejný monolit s hezčími názvy složek — hranice, kterou nikdo nevynucuje, žádná hranice není. Vynucuj ji (lint pravidla, oddělená schémata, code review), ne jen doufej.
Anti-patterny (čeho se vyvarovat)
- 🚩 Byznys logika v kontroleru / v databázovém modelu — nejde testovat ani znovupoužít.
- 🚩 God object — jedna obří třída „UserManager" o 2000 řádcích, co dělá úplně všechno.
- 🚩 Circular dependencies — A potřebuje B, B potřebuje A. Známka špatně vedených hranic.
- 🚩 Premature architecture — pět vrstev a tucet rozhraní na appku se třemi endpointy.
Failure modes — jak to dopadne, když architekturu zanedbáš
- Spaghetti kód → každá změna má nečekané vedlejší efekty, protože všechno souvisí se vším.
- Big ball of mud → žádné jasné hranice; po roce nikdo neví, kde co je.
- Over-engineering → tolik abstrakcí, že přidat jeden endpoint trvá celý den.
- Netestovatelnost → logika přilepená na HTTP a databázi → testy potřebují celý systém.
🛠️ Cvičení
- Rozvrstvi chaos. Vezmi (klidně smyšlený) endpoint, který v jednom kuse kódu validuje vstup, počítá cenu, ukládá do databáze a posílá e-mail. Rozděl ho na controller → service → repository.
- Injektuj závislost. Přepiš
class OrderService { repo = new PostgresRepo() }tak, aby repo dostával v konstruktoru. Pak naskicuj test, kde mu předáš falešné repo bez databáze. - Navrhni DTO. Entita
Usermáid,email,passwordHash,isAdmin,createdAt. Navrhni response DTO pro veřejný profil — co v něm bude a co ne a proč. A co zvaliduješ u request DTO? - Kam patří pravidlo? Pro „uživatel nesmí mít víc než 3 aktivní objednávky" urči, do které vrstvy logika patří a proč ne do kontroleru.
- Anemic vs rich. Naskicuj objekt
Orderdvakrát: jednou anemicky (data + service zvlášť), jednou richly (order.zrus()si hlídá pravidla sám). Pojmenuj výhody a nevýhody. - Podle vrstev vs podle feature. Načrtni stejnou appku (users, orders, billing) v obou strukturách složek a řekni, kdy která začne být nepříjemná.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
- Rozvrstvi chaos — controller jen přijme request, zvaliduje vstup a zavolá service; service spočítá cenu a řídí pravidla; repository ukládá; e-mail je další závislost service (přes port). Pointa: „tencí číšníci, vytížení kuchaři" — past je nechat výpočet ceny nebo SQL v kontroleru, pak to nejde znovupoužít z fronty ani otestovat.
- Injektuj závislost — z
repo = new PostgresRepo()udělášconstructor(private repo: UserRepository); v testu předáš fake repo, které vrací data z paměti, žádná databáze. Na co pozor: typuj parametr na rozhraní (UserRepository), ne na konkrétníPostgresRepo— jinak ti DI nepomůže a fake nepodstrčíš. - Navrhni DTO — response DTO pro veřejný profil:
id,email(pokud má být vidět),createdAt; nikdypasswordHashaniisAdmin(únik). U request DTO vezmeš jen povolená pole a zvaliduješ je. Pointa: nikdy nevracej entitu celou ani nezapisuj naslepo, čím klient pošle — jinak si podstrčíisAdmin: true. - Kam patří pravidlo? — „max 3 aktivní objednávky" je byznys logika → patří do service / doménového modelu, ne do kontroleru. Důvod: v kontroleru jde svázané s HTTP, nejde znovupoužít z fronty/cronu a špatně se testuje (test by potřeboval celý HTTP stack).
- Anemic vs rich — anemic:
Orderje pytlík dat, pravidla vOrderService(jednoduché, ale logika se rozleze); rich:order.zrus()si pravidla hlídá sám (logika u dat, hůř se rozteče). Pointa: obojí je legitimní — ať zvolíš cokoli, logika nikdy nesmí skončit v kontroleru ani v databázové vrstvě. - Podle vrstev vs podle feature — „podle vrstev" (
controllers/,services/) je jasné u malé appky, ale jedna funkce se roztáhne po 5 složkách; „podle feature" (users/,orders/) drží vše pohromadě a líp škáluje. Na co pozor: u feature struktury si vrstvy musíš hlídat uvnitř každé feature, jinak se chaos jen přesune dovnitř.
🧠 Otázky & odpovědi
Proč byznys logika nepatří do kontroleru?
Kontroler je svázaný s HTTP. Logika v něm nejde znovupoužít z jiného vstupu (zpracování fronty, cron, příkazová řádka), špatně se testuje (test potřebuje celý HTTP stack) a má sklon nabobtnat do obří nepřehledné třídy. Patří do service / doménového modelu, který o HTTP ani o konkrétní databázi nic neví. Kontroler má jen přijmout, zvalidovat, zavolat service a vrátit odpověď.
Co říká dependency rule v hexagonální architektuře?
Závislosti směřují dovnitř, k doméně. Byznys logika nezávisí na ničem vnějším; naopak okolní technologie (databáze, HTTP, fronta) se přizpůsobují doméně tím, že implementují její rozhraní (porty). Důsledek: vyměníš databázi nebo HTTP za zpracování fronty a jádro appky se nezmění — a otestuješ ho bez databáze.
Jak dependency injection zlepšuje testovatelnost?
Když si třída sama vyrábí závislosti (new PostgresRepo()), je s nimi pevně srostlá a v testu je nejde
obejít → test potřebuje reálnou databázi. S dependency injection dostane závislost zvenčí, takže
v testu jí podstrčíš falešnou (fake) implementaci. Testy jsou tím rychlejší, spolehlivější a je jasně
vidět, na čem třída závisí.
Jaký je rozdíl mezi entity, value object a aggregate?
Entity má identitu, která přetrvává změny (uživatel #42 i po přejmenování). Value object je
definovaný svou hodnotou a je neměnný (peníze, adresa — dvě stejné jsou zaměnitelné, nemají vlastní
identitu). Aggregate je skupina entit, které se mění jako celek přes jeden hlavní objekt
(objednávku a její položky měníš jen přes Order), čímž drží data konzistentní.
Co je DTO a proč neposílat klientovi přímo databázovou entitu?
DTO (Data Transfer Object) je prostý objekt bez logiky, jehož úkol je přenést data přes hranici (mezi
API a klientem) ve správném tvaru. Databázovou entitu neposílej přímo ze tří důvodů: únik citlivých
polí (passwordHash, isAdmin), provázanost (změna databáze pak rozbije klienty) a jiný
tvar (klient chce data jinak, než leží v databázi). Proto na hranici sestavíš DTO jen s tím, co má
klient vidět (response DTO), a u příchozích dat (request DTO) vezmeš jen povolená pole a zvaliduješ je.
Kdy je architektura over-engineering?
Když přidává abstrakce, které neřeší žádný současný problém. Pět vrstev a tucet rozhraní na jednoduchou appku se třemi endpointy = ceremonie, co jen zpomaluje. YAGNI (You Aren't Gonna Need It): začni jednoduše (klidně controller → service → ORM) a přidávej strukturu, teprve když složitost reálně roste. Architektura má sloužit, ne být sama sobě cílem.
Co je modulární monolit a čím se liší od obyčejného monolitu?
Je to jeden nasaditelný program (jeden deploy, lokální transakce, žádná síťová režie), ale uvnitř rozdělený na moduly s vynucenými hranicemi — každý modul má vlastní doménu i data, ven mluví jen přes veřejné rozhraní a nesahá cizím do tabulek ani neimportuje jejich vnitřnosti. Od obyčejného monolitu se liší právě tou vynucenou hranicí: běžný monolit ji nehlídá a časem se rozteče do „velké koule bláta". Výhoda oproti microservices: jasné hranice bez distribuovaného pekla — a když modul jednou potřebuješ odříznout do služby, hranici už máš hotovou. Proto cesta monolit → modulární monolit → microservices, ne skok rovnou.
Proč učebnice bere jen DDD-lite a co přidává celé DDD?
DDD-lite = taktické vzory (entity, value object, aggregate, ubiquitous language) — stavební kameny kódu, co pomůžou skoro vždy. Celé DDD přidává strategickou vrstvu pro složité domény a víc týmů: bounded contexts (hranice, kde má pojem jeden význam), subdomény (co je jádro vs. podpůrné vs. generické), context mapping / anti-corruption layer (jak kontexty mluví a brání se cizímu modelu) a domain events (dozvídají se o změnách přes události, ne sáháním do dat). Je to investice, která se vyplatí až u složité domény — na CRUD appku je to kanón na vrabce.
