Bounded Context a Context Mapping
Bounded Context určuje hranici, Context Map popisuje, co se na ní děje. Osm pojmenovaných vztahů mezi kontexty – od těsné spolupráce po úmyslné oddělení.
Obsah kapitoly
Kapitola ve videokurzu
Celý videokurzPřehrávač YouTube se načte až po kliknutí.
Strategický design v DDD má dvě stránky. Bounded Context definuje hranici jednoho modelu: co je uvnitř, co venku a kde končí jeden Ubiquitous Language a začíná druhý. Definici rozvádí kapitola Co je DDD, taktické dopady Základní koncepty. Neřeší ale, co se děje na té hranici, když dva kontexty potřebují spolupracovat.
Na to odpovídá Context Mapping. Evans ho popsal v roce 2003 v knize Domain-Driven Design: Tackling Complexity in the Heart of Software, v kapitole 14 (Maintaining Model Integrity), jako vizuální i textovou dokumentaci všech Bounded Contexts v systému a vztahů mezi nimi [1]. Vaughn Vernon v Implementing Domain-Driven Design (2013) disciplínu doplnil o praktické implementační vzory [2]. V praxi Context Map typicky vzniká jako výstup Event Stormingu, kterému patří následující kapitola. Zde jde o osm pojmenovaných vztahů mezi BC a jejich kompromisy. Ukázky v Symfony 8 stojí na CQRS se Symfony Messengerem, REST API a HTTP klientech.
03.01 Co je Context Map a proč ji nakreslit#
Eric Evans vymezuje Context Map jako přehled všech modelů na projektu. Každý dostane jméno a vlastní Bounded Context. U každého bodu dotyku mezi modely se popíše explicitní překlad, všechno sdílené, izolační mechanismy a míra vlivu jedné strany na druhou [1]. Evans k tomu přidává pořadí práce: napřed zmapovat terén tak, jak skutečně vypadá, transformace řešit až potom. Mapa tedy popisuje, co je, ne co by mělo být. S UML diagramem tříd nemá nic společného; je to organizační a politická mapa: kdo s kým mluví, jakým jazykem a kdo rozhoduje, když se jazyk musí změnit.
Context Map má dvě složky:
- Vizuální složka – diagram s krabičkami (Bounded Contexts) a šipkami (vztahy) opatřenými stereotypy (
<<ACL>>,<<OHS>>,U/Dpro upstream/downstream). - Textová složka – krátký dokument popisující každý vztah: odpovědné týmy, kontrakt, frekvenci změn, eskalační kontakt. Váží víc než obrázek, který zastarává rychleji, než se stačí aktualizovat.
Bez mapy zůstává implicitní vztahový graf. Tým A ví, že volá tým B, ale nikdo neřekl, jakým způsobem, kdo kontrakt vlastní a co se stane, když ho někdo jednostranně změní. Implicitní vztahy vedou k chybám v integraci, plíživému sdílení modelů a nakonec k Big Ball of Mud (viz 03.12).
Jak velký je jeden Bounded Context
Evans popisuje Bounded Context jako hranici, uvnitř které platí jeden model, typicky podsystém nebo práci jednoho týmu [1]. O nasazení tam není ani slovo. Fowler jde stejným směrem: hranici určuje lidská kultura a jazyk, technická reprezentace je až důsledek. Připouští proto i dva kontexty uvnitř jediné aplikace [3].
Prakticky jde o tři úrovně, které se v diskusi rády slévají do jedné:
- Bounded Context je logická hranice modelu a jazyka. Odpovídá na otázku, kde přestává slovo Order znamenat totéž.
- Modul je promítnutí té hranice do kódu. V Symfony je to namespace
App\Orderinga adresář pod ním. Jeden BC = jeden modul je rozumné výchozí mapování. - Nasazovací jednotka je provozní rozhodnutí. Modulární monolit nasazuje deset kontextů jako jednu aplikaci a hranice mezi nimi tím neztrácejí platnost.
Rovnice „Bounded Context = microservice“ je tedy zkratka, ne definice. Rozebírá ji kapitola DDD a microservices, mapování subdomén na kontexty popisují Subdomény.
03.02 Osm vztahů a rolí – přehled#
Pojmenovaných vzorů soužití Bounded Contexts je osm. Sedm z nich popsal Eric Evans v Domain-Driven Design (2003), Partnership doplnil později v Domain-Driven Design Reference (volně dostupná edice 2015). Vaughn Vernon v IDDD (2013) katalog rozšířil o nuance a kombinace, jádro pojmenování ale zůstalo. Taxonomie dává sdílený slovník („zde je to Customer/Supplier, ne Conformist“) a zviditelňuje cenu vazby. Některé vzory jsou dražší než jiné a volba mezi nimi je strategická.
Osm jmen přitom neleží na jedné úrovni. Čtyři z nich popisují vztah dvou kontextů, tedy kdo na koho tlačí a jak těsně jsou svázané release cykly. Zbylé čtyři popisují roli jednoho konce asymetrického vztahu: čím upstream integraci umožní a čím se downstream brání. Evans to říká přímo v definici Open Host Service. Poskytovatel služby je tím postaven do upstream pozice, každý klient je downstream a část klientů bude conformisty, část si postaví anti-corruption layer [1]. Nevybírá se tedy jedna z osmi možností, ale vztah a k němu role na obou koncích.
Pozn.: Evans v Domain-Driven Design Reference (2015) uvádí vedle osmi vzorů ještě devátý, Big Ball of Mud převzatý od Foota a Yodera. Probírá ho samostatně sekce 03.12 Anti-vzor: Big Ball of Mud. Na rozdíl od osmi vzorů níže se nenavrhuje: je to stav rozpadu, který vznikne, když žádná volba nepadne.
Úroveň 1: vztah dvou kontextů.
| Vztah | Symetrie | Vazba modelů | Vazba plánování | Kdo o něm rozhoduje |
|---|---|---|---|---|
| Partnership | symetrický | volná (dva modely) | těsná (společný release) | oba týmy |
| Shared Kernel | symetrický | těsná (sdílený kód) | těsná (souhlas obou) | oba týmy souhlasem |
| Customer/Supplier | asymetrický | volná (překlad na hranici) | střední (vyjednávání) | upstream, s vlivem downstreamu |
| Separate Ways | žádný vztah | žádná | žádná | strategické rozhodnutí |
Úroveň 2: role na konci asymetrického vztahu.
| Role | Konec | Co dělá | Vazba downstreamu na cizí model |
|---|---|---|---|
| Open Host Service | upstream | publikuje jeden protokol pro mnoho konzumentů | snižuje |
| Published Language | upstream + standardy | fixuje formát a sémantiku zpráv | snižuje |
| Conformist | downstream | přebírá cizí model 1:1, bez překladu | maximální |
| Anti-Corruption Layer | downstream | překládá cizí model na vlastní | minimální |
Obě úrovně se kombinují. Customer/Supplier běžně využívá Open Host Service jako kanál a Published Language jako formát zpráv; na druhém konci si downstream postaví ACL, nebo se smíří s rolí Conformistu. Vernon (2013) upozorňuje, že souběh OHS, PL a ACL není rozpor. Pro kreslení map zavádí zkratky OHS, PL a ACL, které se v komunitě ujaly [2].
03.03 Partnership#
Partnership je symetrický vztah mezi dvěma Bounded Contexts, jejichž týmy společně uspějí, nebo společně padnou. Sdílí doménový cíl, koordinují plánování a typicky nasazují společným release procesem. Integrační náklady (synchronní porady, společná roadmapa, časté merge konflikty) jsou nižší než cena, kterou by oba týmy zaplatily za nezávislou práci.
Eric Evans vzor zachytil v Domain-Driven Design Reference (2015). Vychází z pozorování: kde by selhání vývoje v kterémkoli ze dvou kontextů znamenalo selhání dodávky pro oba, spolupráce obvykle vznikne sama. Doporučuje ji zformalizovat, tedy zavést koordinované plánování vývoje a společné řízení integrace. Partnership se proto typicky nevolí od stolu. Mapa ho pojmenuje a doplní mu proces.
Evans přidává i konkrétní mechanismus. Zvláštní testovací sada ověří, že rozhraní odpovídá očekáváním klientského systému, a běží v continuous integration serverového systému. Bez ní zůstane z Partnershipu jen kalendář společných porad.
Příklad: Catalog BC + Pricing BC v začínajícím startupu
Fiktivní e-shop: tým Catalog vlastní produktové informace (název, popis, obrázky, kategorie), tým Pricing cenotvorbu (základní cena, slevy, A/B test ceny pro různé segmenty). V rané fázi na sobě závisejí oběma směry: produktovou stránku nejde zobrazit bez ceny a cena bez produktu nemá čeho se týkat.
Týmy proto Partnership pojmenují a zformalizují: jeden produktový manažer pokrývá oba BC, retrospektivy jsou společné, release proces jednotný (deploy obou BC současně). Uspořádání funguje, dokud se domény neusadí natolik, aby mohly žít vlastním tempem.
Symfony detail: monorepo a společný release
Adresářové uspořádání popisuje struktura podle subdomén. Pro Partnership je podstatné, co oba BC v monorepu sdílejí navíc: jeden composer.json, společnou DI registraci v config/services.yaml a jednu konfiguraci message busu v config/messenger.yaml.
Oba BC si přitom drží vlastní namespacy (App\Catalog, App\Pricing) i vlastní invarianty; infrastruktura (DI kontejner, RabbitMQ, databázový server) je společná. Komunikují in-process přes synchronní transport Symfony Messengeru, bez serializovaného JSON po síti.
Anti-vzor: „Partnership jako výchozí volba“
Partnership, který vznikne, aniž by si ho týmy vědomě zvolily, vede přímo k Big Ball of Mud. Typický důvod: „nemáme čas se domluvit, takže to budeme dělat dohromady“. Agregáty jednoho BC začnou číst tabulky druhého „protože je to rychlejší“. Doménové události ustoupí sdíleným service třídám. Po roce nikdo nedokáže říct, kde přesně končí Catalog a začíná Pricing.
Indikátory, že Partnership přestává fungovat
- Týmy začínají odkládat vlastní funkce, protože čekají na druhý tým. Celková rychlost klesá.
- Retrospektivy se opakovaně točí kolem stejných mezitýmových napětí.
- Release proces se prodlužuje, protože koordinace dvou roadmap je příliš nákladná.
- Jeden tým získá výrazně odlišnou prioritu (např. Catalog SEO sprint, zatímco Pricing dělá compliance) a společné nasazení ztrácí důvod.
Pak je čas Partnership rozpustit a přejít na Customer/Supplier nebo Open Host Service mezi oběma BC.
03.05 Customer/Supplier#
Customer/Supplier je asymetrický vztah, ve kterém upstream (supplier) poskytuje data nebo službu a downstream (customer) je konzumuje. Od Conformistu (viz dále) se liší tím, že downstream má hlas. Může od suppliera výslovně požadovat nové funkce, supplier je přijme do backlogu a dohodne termín. Kdy a jak je dodá, rozhoduje ale supplier.
Evans (2003) varuje před oběma extrémy. Právo veta downstream týmu nebo těžkopádné procedury žádostí o změny ochromí volný vývoj upstreamu. Downstream bez vlivu je zase vydán na milost prioritám upstreamu. Řešením je jasný vztah zákazník–dodavatel: downstream vystupuje v plánovacích schůzkách jako zákazník upstreamu, požadavky se vyjednávají a rozpočtují a obě strany znají závazky i termíny.
Příklad: Catalog (supplier) → Ordering (customer)
Ordering BC potřebuje k sestavení objednávky produktové ID a aktuální cenu. Produktová data spravuje Catalog. Data tečou jedním směrem: Ordering čerpá z Catalogu a Catalog ho bere jako „prvotřídního zákazníka“, aniž by ztratil svobodu rozhodovat o vlastním modelu.
Když Ordering tým řekne „potřebujeme v product DTO i availableStock“, Catalog tým to neudělá okamžitě. Posoudí, zda to do Catalog modelu patří. Stav skladu vlastní Warehouse BC (viz mapování subdomén na kontexty). Catalog z něj přebírá jen odvozenou dostupnost pro produktový výpis, takže pole do produktového DTO přidat může. Požadavek proto naplánuje do následujícího sprintu a dodá. Kdyby do modelu nezapadal, navrhl by alternativu, třeba aby Ordering četl dostupnost přímo z API Warehouse BC.
Ukázka kódu: Symfony Messenger external transport
Customer/Supplier se v Symfony 8 typicky staví na asynchronních událostech. Catalog publikuje ProductPriceChanged do AMQP exchange, Ordering ho konzumuje přes vlastní Messenger transport.
1# config/packages/messenger.yaml – downstream Ordering BC2framework:3 messenger:4 transports:5 from_catalog:6 dsn: '%env(CATALOG_AMQP_DSN)%'7 options:8 exchange:9 name: 'catalog.events'10 type: 'topic'11 queues:12 ordering.from_catalog:13 binding_keys: ['product.price_changed', 'product.discontinued']14 # Služba implementující Messenger SerializerInterface,15 # překládá JSON od Catalogu na lokální třídy Orderingu16 serializer: 'App\Ordering\Infrastructure\Messaging\CatalogEventSerializer'17 # Vlastní serializer konzumenta bývá decode-only. Retry ale18 # posílá zprávu zpět přes sender téhož transportu, tedy přes19 # encode() - a ten skončí výjimkou, která shodí workera.20 # Proto nula pokusů a rovnou failure transport. Retry sem21 # patří teprve tehdy, když serializer umí i encode().22 # Rozbor v kapitole DDD a microservices, sekce 19.08.23 retry_strategy:24 max_retries: 025 26# Příjem zajišťuje worker: php bin/console messenger:consume from_catalog --bus=event.bus27# (cizí zpráva nenese BusNameStamp, bez --bus by šla na výchozí sběrnici)
A handler v Orderingu:
1<?php2 3declare(strict_types=1);4 5namespace App\Ordering\Application\EventHandler;6 7use App\Ordering\Application\ExternalEvent\ProductPriceChanged;8use App\Ordering\Domain\PriceCache;9use Symfony\Component\Messenger\Attribute\AsMessageHandler;10 11#[AsMessageHandler(bus: 'event.bus')]12final class ProductPriceChangedHandler13{14 public function __construct(15 private readonly PriceCache $priceCache,16 ) {}17 18 public function __invoke(ProductPriceChanged $event): void19 {20 // Ordering si drží lokální projekci ceny – eventually consistent.21 $this->priceCache->update(22 productId: $event->productId,23 newPrice: $event->newPriceCents,24 currency: $event->currency,25 effectiveAt: $event->occurredAt,26 );27 }28}
Stabilní kontrakt jako základ
Customer/Supplier vyžaduje stabilní kontrakt. Bez něj je každá změna upstream modelu breaking change pro všechny downstream konzumenty. V praxi se proto kombinuje s Open Host Service jako kanálem a Published Language jako formátem zpráv (sekce 03.08 a 03.09).
Plánovací rituály mezi týmy
Customer/Supplier potřebuje aspoň minimální koordinační rituály:
- Pravidelný společný grooming (typicky 1× za sprint), kde downstream prezentuje své požadavky.
- Dokumentovaná roadmapa upstreamu – downstream musí vidět, co se chystá a kdy čekat breaking changes.
- Eskalační kanál – kdo rozhoduje, když se týmy neshodnou? Typicky produktový manažer nebo architekt.
Bez nich sklouzne Customer/Supplier do Conformistu: downstream přestane mít hlas a jen se přizpůsobuje.
Kontrakt vynucený testy, ne dohodou
Rituály pokrývají organizační polovinu vzoru. Evans (2003) přidává technickou: společně vyvinuté automatizované akceptační testy ověřují očekávané rozhraní upstreamu. Patří do testovací sady upstream týmu a běží v jeho continuous integration. Upstream pak může měnit implementaci beze strachu z vedlejších efektů dole – rozbitý downstream kontrakt shodí build u něj, ne až v produkci u zákazníka.
Dnes má tento mechanismus jméno i nástroje. Ian Robinson ho v roce 2006 popsal jako consumer-driven contracts [4]. Konzument formuluje očekávání, poskytovatel je drží zelené ve své pipeline. V PHP k tomu slouží Pact z balíčku pact-foundation/pact-php. Vyplatí se všude, kde má upstream víc než jednoho konzumenta a breaking change je lepší odhalit dřív než telefonátem.
03.06 Conformist#
Conformist je role downstreamu v asymetrickém vztahu: downstream vědomě rezignuje na vlastní model a přebírá upstream model 1:1, bez překladu a bez validace. Je to úspora na hranici, kde boj o vlastní model nestojí za to.
Evans (2003) situaci popisuje bez příkras: když upstream nemá motivaci vycházet potřebám downstream týmu vstříc, je downstream bezmocný. Otrocké převzetí upstream modelu odstraní složitost překladu mezi kontexty. Je-li upstream design dost dobrý nebo kompatibilní, nemusí to způsobit větší potíže. Jeden zisk Evans uvádí výslovně: s upstream týmem začnete sdílet Ubiquitous Language.
Kdy Conformist zvolit
- Externí dodavatel bez navazující doménové logiky – data ze SaaS (Stripe, Shopify) se jen zobrazují nebo přeposílají a bojovat proti jejich modelu nemá smysl. Jakmile na nich stojí vlastní pravidla, jako platby v objednávce nebo identita uživatele, patří mezi SaaS a doménu ACL.
- Regulátor – banka přijímá ISO 20022 zprávy. Boj proti formátu by byl boj proti standardu.
- Reporting nebo dashboard BC, který data jen přebírá a zobrazuje.
- Krátkodobé řešení, dokud se investice do ACL nevyplatí.
Příklad: Reporting BC přijímá Stripe payment objekty 1:1
1<?php2 3declare(strict_types=1);4 5namespace App\Reporting\Application;6 7// Conformist: žádné vlastní VO, používáme přímo Stripe SDK objekty8use Stripe\PaymentIntent;9 10final class StripePaymentReportRepository11{12 public function __construct(13 private readonly \Stripe\StripeClient $stripe,14 ) {}15 16 /**17 * @return list<PaymentIntent>18 */19 public function getPaymentsInMonth(int $year, int $month): array20 {21 $from = new \DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));22 23 $payments = $this->stripe->paymentIntents->all([24 'created' => [25 'gte' => $from->getTimestamp(),26 'lt' => $from->modify('+1 month')->getTimestamp(),27 ],28 'limit' => 100,29 ])->autoPagingIterator(); // stránkuje přes všechny platby měsíce30 31 return iterator_to_array($payments, false);32 }33 34 public function listMonthlyPayments(int $year, int $month): array35 {36 $payments = $this->getPaymentsInMonth($year, $month);37 38 return array_map(39 // Reporting používá pole ze Stripe tak, jak jsou – currency, amount,40 // status. Žádný překlad na Money VO, žádné české doménové pojmy.41 fn(PaymentIntent $p) => [42 'id' => $p->id,43 'amount' => $p->amount, // Stripe používá centy44 'currency' => $p->currency, // 'usd', 'eur' lower-case45 'status' => $p->status,46 ],47 $payments,48 );49 }50}
Reporting je vůči Stripe vědomě Conformist. Žádný převod na Money VO, žádný překlad 'usd' → Currency::USD. Když Stripe přejmenuje pole nebo přidá nový status, Reporting změnu přijme. Zisk je nulová investice do ACL, cena křehkost vůči neslučitelným změnám upstreamu.
Kompromis Conformistu
Conformist ušetří:
- Kód překladu (DTO → VO mapování).
- Pochopení dvou modelů namísto jednoho.
- Údržbu testů ACL vrstvy.
Conformist zaplatí:
- Při každé neslučitelné změně upstreamu se downstream musí přepsat.
- Doménová logika downstreamu používá pojmy upstreamu a srozumitelnost tím trpí.
- Když upstream službu ukončí, musí downstream přepsat všechno, co z jeho modelu přejal.
- Druhý upstream se přidává těžko (PayPal vedle Stripe), protože celá doménová logika kopíruje Stripe.
U proprietárního dodavatele vychází bilance záporně, jakmile na převzatém modelu stojí vlastní doménová logika: přejímáte cizí model, který se mění podle cizí roadmapy. U integrace se standardem se obrací. ISO 20022, iCalendar nebo CloudEvents definují stabilní, veřejně popsané pojmy, které čte celý obor. Conformita vůči standardu přináší sdílený jazyk s každým dalším účastníkem trhu.
Conformist jako přechodný stav
Conformist bývá přijatelný dočasně, třeba když projekt potřebuje rychle dodat MVP a integrace s upstreamem nesnese odklad. Rozumné je pak ho zvolit, ale zapsat technický dluh do backlogu: „za 6 měsíců, až budeme vědět, jak Reporting používáme, postavíme ACL“. Bez zápisu Conformist „uzraje“ v trvalé řešení a refaktor je pak dvojnásob bolestivý.
03.07 Anti-Corruption Layer (ACL)#
Anti-Corruption Layer (ACL) je izolační vrstva mezi downstream doménovým modelem a cizím (legacy, externím, nevstřícným) protějškem. Překládá oběma směry, validuje vstupní data a filtruje neplatné stavy dřív, než dorazí do domény. Sahá se po něm tam, kde downstream nemá vliv na upstream, ale vlastní model chránit chce. Chyby v jeho implementaci bývají nenápadné, protože cizí pojmy prosakují do domény postupně.
Evans (2003) rozlišuje dvě situace. Mezi dobře navrženými kontexty se spolupracujícími týmy může být překladová vrstva prostá. Když ale chybí kontrola nebo komunikace potřebná pro Shared Kernel, Partnership či Customer/Supplier, překlad se stává obranou. Downstream klient si pak vytvoří izolační vrstvu, která mu funkčnost upstream systému zpřístupní v pojmech jeho vlastního doménového modelu.
Tři odpovědnosti ACL
- Schema mapping – překlad datových struktur: SOAP response, REST DTO nebo CSV řádek na doménová VO a entity. Otázka zní „jak vypadá payload“.
- Concept translation – překlad významu dat. Upstream používá
customerNumberjako int, downstreamCustomerIdjako UUID. Upstream má status"PENDING", downstream enumOrderState::AwaitingPayment. Otázka zní „co to znamená“. - Anti-corruption – validace a filtrace. Záporné částky, chybějící povinná pole, status mimo známý enum, datum v budoucnosti – všechno musí ACL odmítnout, než se to dostane do domény. Otázka zní „je to důvěryhodné“.
Ukázka kódu: kompletní LegacyBillingTranslator
1<?php2 3declare(strict_types=1);4 5namespace App\Ordering\Infrastructure\Acl;6 7use App\Ordering\Domain\Event\InvoicePaid;8use App\Ordering\Domain\ValueObject\InvoiceId;9use App\SharedKernel\Domain\Currency;10use App\SharedKernel\Domain\Money;11use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;12 13/**14 * DTO upstreamu: přesně to, co vrací legacy SOAP endpoint. Doména ho nikdy15 * nevidí - je to vstup ACL, ne jeho výstup.16 */17final readonly class InvoicePaidSoapResponse18{19 public function __construct(20 public string $invoiceNumber,21 public int $amountInCents,22 public string $status,23 public string $paidAtIso,24 public string $currencyCode = 'EUR',25 ) {26 }27}28 29/**30 * ACL mezi legacy Billing systémem (SOAP) a Ordering BC.31 *32 * Tři odpovědnosti:33 * 1. Schema mapping – InvoicePaidSoapResponse => InvoicePaid34 * 2. Concept translation – invoiceNumber (string, např. INV-2025-00042) => InvoiceId (UUID)35 * 3. Anti-corruption – odmítá neplatné stavy z legacy36 */37final class LegacyBillingTranslator38{39 public function translateInvoicePaid(InvoicePaidSoapResponse $r): InvoicePaid40 {41 // (3) Anti-corruption: legacy umí poslat záporný amount jako „credit“42 if ($r->amountInCents < 0) {43 throw new UnrecoverableMessageHandlingException(44 'Negative amount from legacy; credits are not supported in Ordering BC.'45 );46 }47 48 // (3) Anti-corruption: chybějící identifikátor49 if ($r->invoiceNumber === '') {50 throw new UnrecoverableMessageHandlingException('Missing invoiceNumber in legacy payload.');51 }52 53 // (3) Anti-corruption: legacy umí poslat status, kterému nerozumíme54 if ($r->status !== 'PAID') {55 throw new UnrecoverableMessageHandlingException(56 "Unsupported legacy status '{$r->status}'; expected PAID."57 );58 }59 60 // (3) Anti-corruption: chybějící datum. Bez téhle kontroly udělá61 // `new \DateTimeImmutable('')` z prázdného řetězce aktuální čas62 // a doména dostane „zaplaceno právě teď“ místo chyby.63 $paidAt = \DateTimeImmutable::createFromFormat(64 \DateTimeInterface::ATOM,65 $r->paidAtIso,66 );67 68 if ($paidAt === false) {69 throw new UnrecoverableMessageHandlingException(70 "Unparsable paidAt '{$r->paidAtIso}'; expected ISO 8601."71 );72 }73 74 if ($paidAt > new \DateTimeImmutable()) {75 throw new UnrecoverableMessageHandlingException('Legacy sent a future paidAt.');76 }77 78 // (3) Anti-corruption: měnu překládáme, ne přepisujeme. Natvrdo79 // zapsané EUR by tiše změnilo částku na každé faktuře v jiné měně -80 // vrstva proti korupci dat by data sama korumpovala.81 $currency = Currency::tryFrom($r->currencyCode)82 ?? throw new UnrecoverableMessageHandlingException(83 "Unsupported legacy currency '{$r->currencyCode}'."84 );85 86 // (1) Schema mapping + (2) Concept translation87 return new InvoicePaid(88 invoiceId: InvoiceId::fromLegacy($r->invoiceNumber),89 paidAt: $paidAt,90 amount: new Money($r->amountInCents, $currency),91 );92 }93}
Translátor je final třída s jedinou veřejnou metodou. Nemá stav, cache ani vedlejší efekty. Vstupem je upstream DTO, výstupem doménová událost. Na téhle jednoduchosti ACL stojí; translátor se stavem a více vstupy se testuje i udržuje podstatně hůř.
Test ACL
1<?php2 3declare(strict_types=1);4 5namespace App\Tests\Ordering\Infrastructure\Acl;6 7use App\Ordering\Infrastructure\Acl\InvoicePaidSoapResponse;8use App\Ordering\Infrastructure\Acl\LegacyBillingTranslator;9use PHPUnit\Framework\TestCase;10use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;11 12final class LegacyBillingTranslatorTest extends TestCase13{14 public function testTranslatesPaidInvoice(): void15 {16 $translator = new LegacyBillingTranslator();17 18 $event = $translator->translateInvoicePaid(new InvoicePaidSoapResponse(19 invoiceNumber: 'INV-2025-00042',20 amountInCents: 12_345,21 status: 'PAID',22 paidAtIso: '2025-04-29T10:00:00Z',23 ));24 25 $this->assertSame(12_345, $event->amount->amountInCents);26 $this->assertSame('EUR', $event->amount->currency->value);27 }28 29 public function testKeepsLegacyCurrency(): void30 {31 $translator = new LegacyBillingTranslator();32 33 $event = $translator->translateInvoicePaid(new InvoicePaidSoapResponse(34 invoiceNumber: 'INV-3',35 amountInCents: 100,36 status: 'PAID',37 paidAtIso: '2025-04-29T10:00:00Z',38 currencyCode: 'USD',39 ));40 41 $this->assertSame('USD', $event->amount->currency->value);42 }43 44 public function testRejectsMissingPaidAt(): void45 {46 $translator = new LegacyBillingTranslator();47 48 $this->expectException(UnrecoverableMessageHandlingException::class);49 $translator->translateInvoicePaid(new InvoicePaidSoapResponse(50 invoiceNumber: 'INV-4',51 amountInCents: 100,52 status: 'PAID',53 paidAtIso: '', // prázdné pole by se jinak přeložilo na „teď“54 ));55 }56 57 public function testRejectsFuturePaidAt(): void58 {59 $translator = new LegacyBillingTranslator();60 61 $this->expectException(UnrecoverableMessageHandlingException::class);62 $translator->translateInvoicePaid(new InvoicePaidSoapResponse(63 invoiceNumber: 'INV-5',64 amountInCents: 100,65 status: 'PAID',66 paidAtIso: '2099-01-01T00:00:00Z',67 ));68 }69 70 public function testRejectsNegativeAmount(): void71 {72 $translator = new LegacyBillingTranslator();73 74 $this->expectException(UnrecoverableMessageHandlingException::class);75 $translator->translateInvoicePaid(new InvoicePaidSoapResponse(76 invoiceNumber: 'INV-1',77 amountInCents: -100,78 status: 'PAID',79 paidAtIso: '2025-04-29T10:00:00Z',80 ));81 }82 83 public function testRejectsUnknownStatus(): void84 {85 $translator = new LegacyBillingTranslator();86 87 $this->expectException(UnrecoverableMessageHandlingException::class);88 $translator->translateInvoicePaid(new InvoicePaidSoapResponse(89 invoiceNumber: 'INV-2',90 amountInCents: 100,91 status: 'CANCELLED', // legacy občas takhle pošle92 paidAtIso: '2025-04-29T10:00:00Z',93 ));94 }95}
ACL nemá stav a má jediný vstupní bod, takže testy mají tvar dvou tabulek: vstup → výstup a vstup → výjimka. Každý nový okrajový případ z produkce přibude jako nový test.
Anti-vzor: prosakující ACL
ACL nejčastěji selže tak, že cizí pojmy začnou prosakovat do domény. Symptomy:
- Doménová třída
Ordermá polelegacyInvoiceNumber: string. - Doménová událost
OrderShippedmá v payloadustripeChargeId. - Application Service kontroluje
$soapResponse->status === 'PAID'. - ACL třída se rozrůstá do 1000 řádků s mnoha veřejnými metodami a sdíleným stavem.
Pravidlo: ACL drží jednu odpovědnost. Vrstva s desítkami metod a sdíleným stavem už ACL není. Jeden upstream koncept = jeden translátor. Jeho výstupem je vždy doménový VO, entita nebo událost, nikdy surové DTO. Microsoft Azure Architecture Center radí totéž: vrstvu soustředit na překlad a nedávat do ní obchodní pravidla ani orchestraci [5]. Doménová logika v translátoru signalizuje Application Service schovanou v ACL. Vyčleňte ji.
Kdy ACL nestavět
ACL dostává v této kapitole nejvíc prostoru, což svádí brát ho jako výchozí volbu. Tou ale není: vrstva něco stojí a ve třech situacích se investice do ní nevrátí.
Prvním případem je zanedbatelný sémantický rozdíl. Když upstream i downstream používají stejné pojmy ve stejném významu, zbude z ACL prázdná mapovací vrstva, která kopíruje pole z DTO do VO se stejnými jmény. Azure Architecture Center tuto situaci uvádí jako důvod, proč vzor nemusí být vhodný [5]. Když není co překládat, není co stavět.
Druhým případem je latence a provoz. ACL nasazený jako samostatná služba přidává síťový skok, další proces k monitorování a další místo, kde se škáluje. Při vysoké propustnosti se z něj stane úzké hrdlo a ladí se dvakrát: jednou doména, jednou překlad.
Třetí případ je časový. Odchází-li legacy systém za tři měsíce, investice do plnohodnotné vrstvy s testovací sadou se nevrátí. Zda je ACL trvalým prvkem architektury, nebo dočasným lešením pro migraci, patří do textového popisu vztahu na Context Mapě. Jinak lešení zůstane stát navždy.
ACL a Strangler Fig Pattern
Na Anti-Corruption Layeru stojí vzor Strangler Fig pro postupnou migraci z legacy (viz kapitola Migrace z CRUD na DDD). Každý nový BC je od legacy oddělen ACL, dokud legacy nezmizí. Pak ACL většinou zmizí také, nebo se zjednoduší na čistý translátor bez anti-corruption logiky.
03.08 Open Host Service (OHS)#
Open Host Service je role upstreamu, který otevřeně publikuje stabilní dokumentovaný protokol pro mnoho downstream konzumentů. Místo N různých integračních smluv (jeden customer = jeden kontrakt) udržuje jeden veřejný protokol a downstreamy se mu přizpůsobí.
Evans (2003) vychází z pozorování, že úprava translátoru pro každého z mnoha integrujících se sousedů tým utopí. Tým proto definuje protokol, který subsystém zpřístupní jako sadu služeb, a otevře ho všem, kdo integraci potřebují. Protokol se rozšiřuje podle nových integračních požadavků. Výjimkou jsou idiosynkratické potřeby jediného týmu: ty řeší jednorázový translátor, aby sdílený protokol zůstal malý a soudržný.
Kdy zvolit OHS
- 3+ downstream konzumentů. S jedním konzumentem se OHS nevyplatí, stačí Customer/Supplier ad hoc.
- Stabilní doména. Upstream model se mění zřídka, takže se investice do veřejného protokolu vrátí.
- Otevřená integrace, kde konzumentem může být i třetí strana (partneři, white-label klienti, mobilní aplikace).
Implementace v Symfony 8
V Symfony 8 má OHS typicky jednu z těchto podob:
- REST API přes
api-platform/corenebo vlastní controllery, popsané specifikací OpenAPI. - gRPC přes
spiral/roadrunner-grpc, popsané.protosouborem. - Event stream publikovaný přes RabbitMQ / Kafka, popsaný JSON Schema (přechod k PL).
Ukázka kódu: minimální OHS endpoint s verzováním
1<?php2 3declare(strict_types=1);4 5namespace App\Catalog\Infrastructure\Http\OpenHostService;6 7use App\Catalog\Application\Query\GetProductQuery;8use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;9use Symfony\Component\HttpFoundation\JsonResponse;10use Symfony\Component\Messenger\HandleTrait;11use Symfony\Component\Messenger\MessageBusInterface;12use Symfony\Component\Routing\Attribute\Route;13 14final class ProductController extends AbstractController15{16 use HandleTrait;17 18 public function __construct(MessageBusInterface $queryBus)19 {20 $this->messageBus = $queryBus;21 }22 23 /**24 * OHS v1 – stabilní kontrakt zveřejněný v OpenAPI.25 * Verze v URL ('/api/v1/...') je explicitní záruka, že se kontrakt nemění.26 */27 #[Route('/api/v1/products/{id}', name: 'api_v1_product_get', methods: ['GET'])]28 public function getProductV1(string $id): JsonResponse29 {30 $product = $this->handle(new GetProductQuery($id));31 32 return $this->json($product, context: ['groups' => ['ohs.v1']]);33 }34 35 /**36 * OHS v2 – přidává pole 'availableStock'. v1 zůstává funkční pro staré klienty.37 */38 #[Route('/api/v2/products/{id}', name: 'api_v2_product_get', methods: ['GET'])]39 public function getProductV2(string $id): JsonResponse40 {41 $product = $this->handle(new GetProductQuery($id));42 43 return $this->json($product, context: ['groups' => ['ohs.v1', 'ohs.v2']]);44 }45}
Serializační skupiny fungují jen nad objektem s atributy #[Groups]. Když
GetProductQuery vrátí prosté pole, groups v kontextu nic nedělají a obě
verze vrátí totéž – včetně polí, která v1 slibovala nemít. View model
proto potřebuje atributy:
1<?php2 3declare(strict_types=1);4 5namespace App\Catalog\Application\Query;6 7use Symfony\Component\Serializer\Attribute\Groups;8 9final readonly class ProductView10{11 public function __construct(12 #[Groups(['ohs.v1'])]13 public string $id,14 #[Groups(['ohs.v1'])]15 public string $name,16 #[Groups(['ohs.v1'])]17 public int $priceCents,18 // Přidáno ve v2; v1 odpověď ho neobsahuje.19 #[Groups(['ohs.v2'])]20 public int $availableStock,21 ) {22 }23}
V1 a v2 koexistují. Zveřejněná OHS v1 je závazek: jakmile ji nějaký downstream používá, nesmí se rozbít. Bez explicitního verzování to není OHS, jen „REST endpoint s nedostatečnou disciplínou“.
Strategie verzování
Tři běžné přístupy k verzování OHS:
- Verzování v URI (
/api/v1/...) – nejčitelnější, cacheovatelné na úrovni HTTP, doporučené pro veřejné API. - Verzování v hlavičce (
Accept: application/vnd.catalog.v2+json) – čistší URL, ale složitější diagnostika a ladění. - Query parametr (
?api-version=2) – flexibilní, ale svádí k „polovičnímu verzování“, ve kterém v1 nikdy nezmizí.
Politika zastarávání
OHS potřebuje politiku zastarávání. Příklad pro veřejné API:
- Jakmile vyjde nová majoritní verze (v3), dokumentace označí starší verzi (v1) jako zastaralou.
- Hlavičky
Deprecation: @1688169599aSunset: Fri, 31 Dec 2027 23:59:59 GMTse posílají v každé odpovědi v1. - Minimálně 6 měsíců před odstraněním v1 dostanou všichni známí klienti oznámení.
- Po odstranění v1 vrací
410 Gones odkazem na migrační průvodce.
Formát obou hlaviček předepisují dvě různá RFC, která se v praxi často zaměňují. Deprecation definuje RFC 9745 (Standards Track, březen 2025) jako strukturované pole typu Date, tedy unixový timestamp se zavináčem [6]. Starší návrhy povolovaly hodnotu true; ta dnes platná není. Sunset pochází z RFC 8594 a používá datum v HTTP formátu [7]. Časový bod v Sunset nesmí předcházet ten v Deprecation.
03.09 Published Language (PL)#
Published Language je dobře dokumentovaný, formálně specifikovaný formát zpráv mezi Bounded Contexts, nezávislý na programovacím jazyce, frameworku i databázi. Každý konzument si ho může přečíst, validovat proti němu a generovat z něj kód.
Evans (2003) doporučuje jako společné komunikační médium dobře dokumentovaný sdílený jazyk, který vyjádří potřebné doménové informace; do něj a z něj se podle potřeby překládá. Podstatné je zdůvodnění, proč tím jazykem nemá být přímo doménový model jednoho z kontextů. Model použitý jako výměnný formát zmrzne. Přestane reagovat na potřeby vývoje, protože každá jeho změna mění veřejný kontrakt. Vernon (2013) k tomu dodává, že Published Language sahá dál než schema. Je to Ubiquitous Language pro integraci: pojmenovává koncepty, jejich invarianty i sémantiku.
OHS vs. PL – kanál vs. formát
Vztah OHS a PL se často plete. Rozdíl:
- OHS je kanál – REST endpoint, gRPC service, AMQP exchange. „Jak se data dostanou ven.“
- PL je formát – JSON Schema, OpenAPI, Avro, Protocol Buffers. „Jak data vypadají a co znamenají.“
OHS bez PL je REST endpoint vracející ad-hoc JSON, proti kterému downstream nemá jak validovat. Opačná kombinace, PL bez OHS (Avro schema na disku), selhává jinak: nikdo neví, jak data získat. Plnohodnotná veřejná integrace = OHS + PL.
Příklady standardů PL
- JSON Schema (json-schema.org) – nejběžnější pro REST a event payloady.
- OpenAPI (openapis.org) – popis kompletního REST API včetně paths, parameters, schemas.
- AsyncAPI (asyncapi.com) – analogie OpenAPI pro asynchronní (eventní) integrace.
- CloudEvents (cloudevents.io) – CNCF specifikace obálky pro eventy (typ, source, id, time).
- Avro / Protobuf – binární formáty s povinným schématem, oblíbené pro Kafku a gRPC.
Ukázka kódu: JSON Schema pro OrderPlaced event
1{2 "$schema": "https://json-schema.org/draft/2020-12/schema",3 "$id": "https://example.com/events/order-placed-v1.json",4 "title": "OrderPlaced",5 "description": "Integrační událost publikovaná po úspěšném vytvoření objednávky v Ordering BC.",6 "type": "object",7 "required": ["eventId", "orderId", "customerId", "totalAmountCents", "currency", "occurredAt"],8 "properties": {9 "eventId": {10 "type": "string",11 "format": "uuid",12 "description": "Unikátní ID této instance eventu (pro deduplikaci na konzumentech)."13 },14 "orderId": {15 "type": "string",16 "format": "uuid",17 "description": "ID objednávky v Ordering BC."18 },19 "customerId": {20 "type": "string",21 "format": "uuid",22 "description": "ID zákazníka v Identity BC. Stabilní napříč BC."23 },24 "totalAmountCents": {25 "type": "integer",26 "minimum": 0,27 "description": "Celková částka v nejmenší jednotce měny (centech)."28 },29 "currency": {30 "type": "string",31 "pattern": "^[A-Z]{3}$",32 "description": "ISO 4217 kód měny (EUR, CZK, USD)."33 },34 "occurredAt": {35 "type": "string",36 "format": "date-time",37 "description": "ISO 8601 timestamp v UTC, kdy upstream event vyvolal."38 }39 }40}
Schéma se publikuje na URL https://example.com/events/order-placed-v1.json jako kanonický kontrakt, proti kterému validuje producent i každý konzument. Chybí v něm záměrně "additionalProperties": false. Konzument ověřuje povinná pole a neznámá pole ignoruje (tolerant reader). Nové nepovinné pole, například shippingAddressId, proto přibude ve v1 a konzumenti ho začnou číst, až ho budou potřebovat. Schéma order-placed-v2.json vzniká jen při rozbíjející změně: odebrání či přejmenování pole, změně typu nebo významu. Obě verze pak koexistují nejméně po dobu okna zastarávání.
Validace proti schématu v Symfony
1<?php2 3declare(strict_types=1);4 5namespace App\Ordering\Infrastructure\PublishedLanguage;6 7use Opis\JsonSchema\Errors\ErrorFormatter;8use Opis\JsonSchema\Validator;9use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;10 11final class OrderPlacedValidator12{13 public function __construct(14 private readonly Validator $validator,15 ) {16 // URI schématu je identifikátor, ne adresa ke stažení.17 // Opis po síti nesahá – soubor je nutné zaregistrovat.18 $this->validator->resolver()?->registerFile(19 'https://example.com/events/order-placed-v1.json',20 __DIR__ . '/schema/order-placed-v1.json',21 );22 }23 24 public function validate(string $json): void25 {26 $data = json_decode($json, false, flags: JSON_THROW_ON_ERROR);27 28 $result = $this->validator->validate(29 $data,30 'https://example.com/events/order-placed-v1.json',31 );32 33 if (!$result->isValid()) {34 $errors = (new ErrorFormatter())->format($result->error());35 throw new UnrecoverableMessageHandlingException(36 'Payload does not match OrderPlaced v1 schema: ' . json_encode($errors),37 );38 }39 }40}
Ukázka staví na knihovně opis/json-schema, která vrací strukturovaný popis chyby; alternativou je justinrainbow/json-schema. URI v validate() slouží jen jako identifikátor schématu. Knihovna ho po síti nenačte, dokud ho nezaregistrujete pomocí registerFile(), registerRaw() nebo registerPrefix(). Schéma tak zůstává verzovaným souborem v repozitáři, ne vzdálenou závislostí za běhu. Na straně konzumenta je validace proti schématu první krok ACL. Neprojde-li payload, validátor vyhodí UnrecoverableMessageHandlingException a zpráva skončí v dead letter queue. Bez této validace se downstream BC vystavuje všem chybám upstreamu.
03.10 Separate Ways#
Separate Ways je strategické rozhodnutí, že dva Bounded Contexts nebudou integrovány vůbec. Tým přijímá duplicitu dat nebo paralelní procesy, protože propojení by stálo víc, než přinese. Jako jediný ze vzorů znamená „žádný vztah“.
Evans (2003) připomíná, že integrace je vždy drahá a přínos někdy bývá malý. Bounded Context lze proto prohlásit za nepropojený s ostatními. Vývojáři pak v takto malém rozsahu najdou specializovaná řešení bez ohledu na zbytek systému.
Příklad: Marketing BC posílá maily přes vlastní SendGrid
Identity BC drží hlavní seznam zákazníků s preferencemi. Marketing BC posílá hromadné mailové kampaně. Integrovat je jde dvěma způsoby:
- Identity by publikovala událost
CustomerEmailChangeda Marketing by si držel projekci. - Marketing by před každým odesláním ověřoval u Identity, zda má zákazník opt-in.
Marketingový tým si spočítá: 50 kampaní ročně, integrace stojí 200 hodin vývoje a 8 hodin údržby měsíčně. Riziko špatně synchronizovaného opt-in stavu zůstane nenulové i s integrací. Místo toho přijme Separate Ways:
- Marketing si drží vlastní mailing list v SendGridu. Souhlas s obchodními sděleními se uděluje i odvolává jen tam.
- Odhlášení přes odkaz v patičce nebo přes hlavičku
List-Unsubscribezapíše SendGrid do globálního seznamu odhlášených. - Identity o marketingovém souhlasu nic neví. Stránka s preferencemi v zákaznickém účtu jen odkazuje na centrum preferencí v SendGridu.
Řešení má své ústupky. Zákazník spravuje e-maily jinde než zbytek účtu a Marketing nemůže cílit kampaně podle dat z Identity. Zato je levné a odhlášení platí hned, jak požaduje GDPR (čl. 7 odst. 3 a čl. 21 odst. 3) i zákon č. 480/2004 Sb. Rozhodnutí padlo vědomě a stojí na jediné podmínce: souhlas žije na jednom místě. Kdyby se zákazník mohl odhlásit i v Identity, Marketing by se o tom nedozvěděl a každá další kampaň by byla porušením zákona, ne ústupkem.
Kdy Separate Ways zvážit
- Integrace s malou hodnotou – synchronizovaná data uživateli výrazně nepomohou.
- Drahá synchronizace – integrace by vyžadovala distribuovaný konsensus, eventually consistent projekce, složitou retry logiku.
- Krátká životnost jednoho z BC – marketingový nástroj na kampaně se mění každé 2 roky; investice do hluboké integrace se nevyplatí.
- Externí SaaS bez kvalitního API – integrace by stejně byla nestabilní.
Anti-vzor: „Separate Ways z lenosti“
Separate Ways je strategické rozhodnutí, ne výmluva, jak se vyhnout práci. Když tým prohlásí „my to nebudeme integrovat, je to příliš složité“ bez vyčíslení nákladů a hodnoty, nejde o Separate Ways, ale o skrytý technický dluh. Skutečný Separate Ways má zdokumentovanou alternativu (jak se problém vyřešil bez integrace) i okrajový případ (co se stane, když se to rozpadne).
03.11 Praktický postup – jak nakreslit Context Map za 90 minut#
Context Map nepíše jeden architekt o samotě. Je to týmové cvičení s lidmi, kteří znají všechny BC v systému. Doporučená velikost skupiny: 3–8 lidí. Nástroj: tabule, sticky notes, fixy. Digitální nástroje (Miro, FigJam) jsou v pořádku, u fyzické tabule ale vyjde najevo víc napětí mezi týmy než online.
Pět kroků workshopu
-
(0–15 min) Vyjmenovat všechny Bounded Contexts. Sticky note pro každý BC, jméno + 1 věta popisu („Catalog: produktové info“, „Pricing: cena včetně slev“). Víc než 12 BC v jednom produktu je varovný signál: možná je modelujete příliš jemně. Celá firma s desítkami týmů jich má přirozeně víc, mapa se pak kreslí po produktech.
-
(15–45 min) Pro každou dvojici BC, která spolu interaguje, nakreslit šipku. Šipka = směr toku dat / kauzality. Pojmenovat vztah (Partnership, Shared Kernel, Customer/Supplier, Separate Ways) a k němu role na obou koncích (OHS, PL nahoře; Conformist, ACL dole). Pokud se tým neshodne („je to Customer/Supplier, nebo Conformist?“), je vztah nedefinovaný a stojí za eskalaci. Označit žlutým fixem.
-
(45–60 min) Označit upstream (U) a downstream (D). Na každé šipce napsat U na straně, která rozhoduje, a D na straně, která se přizpůsobuje. Pokud nikdo neví, kdo je U a kdo D, vztah není pojmenovaný. Eskalace.
-
(60–80 min) Identifikovat „nebezpečné“ vztahy. Conformist k upstreamu, který se rychle mění; Big Ball of Mud (vícenásobné nepojmenované vztahy mezi stejnými BC); Shared Kernel, který přerůstá; Partnership, který už nemá doménový důvod. Pro každý nebezpečný vztah vytvořit konkrétní úkol (Jira ticket / návrh ADR).
-
(80–90 min) Zachytit výsledek. Vyfotit tabuli, vložit do
docs/context-map.pngv repu, doprovázet Markdown souboremdocs/context-map.mds textovým popisem každého vztahu (kdo vlastní, kontrakt, frekvence změn, eskalační kontakt). Vlastník mapy: architekt nebo tech lead.
Co dát do textového popisu vztahu
1## Catalog -> Ordering (Customer/Supplier + OHS + PL)2 3- **Upstream tým**: @catalog-team4- **Downstream tým**: @ordering-team5- **Kanál**: AMQP topic `catalog.events`6- **Schema**: https://schemas.example.com/catalog/product-v2.json7- **Frekvence změn**: ~1× kvartál (minor), 1× rok (major)8- **Eskalační kontakt**: @lead-architect9- **SLA**: 99,9 % zpráv doručeno do 5 s; nečitelná zpráva jde bez opakování do failure transportu (`max_retries: 0`)10- **Onboarding**: docs/onboarding/consume-catalog-events.md
Verzování Context Mapy
Context Map je živý dokument:
- Datum poslední aktualizace v patičce je povinné.
- Markdown i SVG/PNG leží v gitu a verzují se s kódem.
- Revize následuje po každé větší architektonické změně (nový BC, zánik BC, změna typu vztahu).
- Plánovaná revize 1× za 6 měsíců proběhne, i když se nic „nestalo“. Často se ukáže, že se stalo, jen to nikdo nezdokumentoval.
Mapu lze držet i jako zdrojový kód. Context Mapper k tomu má DSL, které odděluje typ vztahu od role na jeho konci a diagram generuje při CI. Zápis VoyagePlanning [D,ACL] <- [U,OHS,PL] Location říká totéž co dvě tabulky v sekci 03.02, ale dá se revidovat v pull requestu.
Jedna velká mapa celého systému stárne nejrychleji. Komunitní praxe DDD Crew doporučuje opak: víc menších map, každou ke konkrétní otázce („jak se dostaneme z Conformistu vůči billingu?“), a u každé mapy vysvětlit použité vzory lidem, kteří o nich rozhodují [8].
03.12 Anti-vzor: Big Ball of Mud#
Big Ball of Mud popsali Brian Foote a Joseph Yoder v roce 1997 v eseji Big Ball of Mud [9]: „A Big Ball of Mud is haphazardly structured, sprawling, sloppy, duct-tape and bailing wire, spaghetti code jungle.“ V jazyce Context Mappingu je to systém, kde každý BC „nějak“ mluví s každým a sdílí s ním databázové tabulky, entity i ad-hoc service vrstvy. Context Map nikdo nenakreslí, protože vztahy nejsou pojmenované.
Symptomy
- Sdílená databáze mezi více BC, kde každý BC čte (a často píše) tabulky druhých.
- Cyklické závislosti mezi BC – A volá B volá C volá A.
- Doctrine entity sdílené mezi BC – jedinou třídu
Orderpoužívají Catalog, Pricing i Billing, každý s jinými očekáváními. - Service třídy s 50+ veřejnými metodami, které „obstarají všechno“.
- Žádný dohodnutý integrační kontrakt – komunikace přes přímé dotazy do DB, sdílené Redis klíče, file system.
- Nikdo neumí říct, „kde končí jeden BC a začíná druhý“.
Proč k tomu dochází
Foote a Yoder upozorňují, že Big Ball of Mud je v praxi nejrozšířenější architektura. Nikdo ji nevolí; vzniká sama, když:
- Tým je pod tlakem dodat funkčnost rychle a nemá čas přemýšlet o hranicích.
- Noví inženýři kopírují existující vzory, které jsou samy o sobě špatné.
- Architekt(i) chybí nebo jsou ignorováni.
- Refaktoring je politicky obtížný (přidává riziko ke krátkodobému dodání).
Cesta ven
Evansova rada v DDD Reference (2015) zní překvapivě pasivně: obtáhnout kolem celého nepořádku hranici a prohlásit ji za big ball of mud. Uvnitř se nepokoušet o sofistikované modelování a hlídat sklon takových systémů rozlévat se do sousedních kontextů. Je to obranné opatření, ne rezignace. Nepojmenovaný nepořádek roste dál; pojmenovaný má aspoň hranici, za kterou se čistý model brání.
Zevnitř ven nejčastěji funguje Strangler Fig: postupně vyčleňovat čisté BC, každý obklopit ACL a přesouvat funkčnost ze staré spaghetti vrstvy do nového modelu. Detail viz Migrace z CRUD na DDD.
Přepis od nuly přitom není tabu. Foote a Yoder mají mezi svými sedmi vzory i Reconstruction: zahodit systém a postavit ho znovu je u původních autorů přijatelná volba, ne selhání [9]. Mezi postupným škrcením a rekonstrukcí se rozhoduje podle rozpočtu a rizika, ne podle víry.
Konkrétní projevy anti-vzorů v PHP a Symfony 8 a strategie jejich nápravy rozebírá kapitola Anti-vzory a typické chyby.
03.13 Shrnutí#
Context Map je vizuální a textová dokumentace všech Bounded Contextů v systému a vazeb mezi nimi. Zachycuje především organizační a politickou realitu – kdo na koho čeká, kdo komu diktuje formát, kdo s kým vůbec mluví.
Osm vzorů z této kapitoly neleží v jednom seznamu, ale ve dvou rovinách. Vztah dvou kontextů popisují Partnership, Shared Kernel, Customer/Supplier a Separate Ways; roli jednoho konce toho vztahu pak Open Host Service a Published Language nahoře, Conformist a Anti-Corruption Layer dole. Vybírá se tedy vztah a k němu role na obou koncích.
Z rolí si nejvíc pozornosti zaslouží ACL. Vyžádá si ho skoro každá netriviální integrace s legacy nebo externím systémem a nese tři odpovědnosti: schema mapping, concept translation a vlastní anti-corruption. Na upstream straně stojí dvojice OHS a Published Language, kde OHS je kanál a PL formát. Bez politiky verzování ale z publikovaného kontraktu zbude jen deklarace.
Když mapa nejde nakreslit, systém je Big Ball of Mud. Cesta ven z něj nezačíná v kódu, ale deskriptivní mapou toho, jak systém vypadá dnes.
Jako discovery workshop pro kreslení mapy slouží Event Storming: v jediném sezení odhalí hranice BC i vztahy mezi nimi. Propojení s organizačním designem popisuje kapitola Team Topologies. Podle Conway's Law architektura kopíruje komunikační strukturu organizace, takže Context Map a rozdělení týmů musí odpovídat jedno druhému. Kde neodpovídají, přizpůsobí se dříve nebo později architektura týmům.
Časté otázky
Jak často aktualizovat Context Map?
Při každé větší architektonické změně (nový BC, zánik BC, změna typu vztahu) okamžitě, a k tomu plánovaná revize minimálně 1× za 6 měsíců. Pokud nemáte čas vizuální složku udržovat aktuální, ponechte si alespoň textový popis (docs/context-map.md), který zastará pomaleji než obrázek. Datum poslední aktualizace v patičce je povinné. Detail v sekci 03.11 Praktický postup.
Můžu mít více než 1 typ vztahu mezi 2 BC?
Ano, je to běžné a často nutné. Customer/Supplier popisuje organizační vztah (kdo rozhoduje, kdo prosí), Open Host Service popisuje technický kanál a Published Language popisuje formát. Tyto tři se typicky kombinují do jednoho komplexního vztahu. Anti-Corruption Layer je technika downstream strany, která se před upstream modelem chrání překladem. V téže pozici je alternativou ke Conformistu, který cizí model přijímá bez překladu. Při kreslení mapy stačí na šipku napsat všechny relevantní stereotypy: <<Customer/Supplier>> <<OHS>> <<PL>>.
ACL vs. Adapter – jaký je rozdíl?
Adapter (z Hexagonal Architecture / GoF) je technický vzor: třída, která implementuje port a překládá volání na konkrétní knihovnu (Doctrine, HTTP klient, Redis). ACL je strategický vzor: vrstva, která chrání váš doménový model před modelem cizího Bounded Contextu. ACL se typicky staví z Adapterů, ale ne každý Adapter je ACL. Má navíc tři specifické odpovědnosti, které „obyčejný“ Adapter nemá: schema mapping, concept translation a anti-corruption (filtraci). Detail v sekci 03.07.
Co dělat, když si všimnu Conformist vztahu, který tam neměl být?
Tři kroky. (1) Ověřte, že je to opravdu nechtěný Conformist, protože někdy jde o vědomé strategické rozhodnutí, které tým zapomněl zdokumentovat. (2) Pokud je nechtěný, vyčíslete cenu jeho opravy: kolik doménových pojmů upstreamu prosáklo do downstream modelu, kolik testů by bylo třeba přepsat, jak často upstream dělá breaking changes. (3) Otevřete ADR (Architecture Decision Record) s návrhem migrace na ACL, typicky inkrementální, jeden translátor za sprint. Dokud není ADR schválené, držte si Conformist jako známý technický dluh v backlogu, ne jako překvapení v produkci.
Je Context Map součást Architecture Decision Record (ADR)?
Context Map sama o sobě není ADR, ale průběžně udržovaný stav; ADR popisuje konkrétní rozhodnutí v čase. Každá změna Context Mapy by ale měla mít ADR: „Změnili jsme vztah Catalog ↔ Pricing z Partnership na Customer/Supplier, protože…“. ADR pak slouží jako historie změn Context Mapy a dává budoucím inženýrům kontext, proč je mapa taková, jaká je. V repu typicky drží mapa docs/context-map.md, ADR docs/adr/0023-rozdeleni-catalog-pricing.md.
Jak Context Map kreslit v textu, ne nástrojem?
Pro malé systémy (do 5 BC) stačí textová Context Map v Markdownu. Formát: pro každý vztah jeden odstavec s polotučnou hlavičkou ve tvaru **Catalog -> Ordering**, šipka určuje směr (upstream → downstream), v textu typ vztahu (Customer/Supplier + OHS + PL), kontrakt, kontakt. Výhody: žádný nástroj, git review beze změny pipeline, full-text search. Nevýhody: chybí vizuální „aha“ efekt. Doporučení: textová verze vždy, vizuální verze (PlantUML, Mermaid, Excalidraw) navíc pro systémy s 5+ BC. PlantUML zdrojový kód lze držet vedle Markdownu a renderovat při CI.
03.14 Další četba#
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, kap. 14 „Maintaining Model Integrity“ (Addison-Wesley, 2003) [1].
- Eric Evans, Domain-Driven Design Reference: Definitions and Pattern Summaries (2015) – stručné definice všech vzorů včetně Partnership a Big Ball of Mud; volně dostupná na domainlanguage.com.
- Vaughn Vernon, Implementing Domain-Driven Design, kap. 3 „Context Maps“ (Addison-Wesley, 2013) [2].
- Brian Foote & Joseph Yoder, Big Ball of Mud, PLoP 1997 [9].
- Martin Fowler, Bounded Context (bliki) [3].
- Vaughn Vernon, Domain-Driven Design Distilled (Addison-Wesley, 2016) – zkrácená přístupnější verze pro úvod do strategického designu.
- DDD Crew, Context Mapping shareable resources [8] – komunitní vizuální notace pro Context Mapping.
- Microsoft Azure Architecture Center, Anti-Corruption Layer pattern [5] – náklady vrstvy a situace, kdy ji nestavět.
- IETF, RFC 9745 The Deprecation HTTP Response Header Field (2025) [6] a RFC 8594 The Sunset HTTP Header Field (2019) [7].
- Vlad Khononov, Learning Domain-Driven Design (O'Reilly, 2021), kap. 4 „Integrating Bounded Contexts“ – novější pohled na volbu vzoru podle poměru nákladů na duplicitu a na koordinaci.
- Ian Robinson, Consumer-Driven Contracts: A Service Evolution Pattern [4] – technická podoba Customer/Supplier vztahu.
- Context Mapper – DSL a generátor map, který odděluje typ vztahu od role a mapu drží v gitu.
- DDD Crew, DDD Starter Modelling Process – zasazení Context Mappingu mezi ostatní strategické artefakty.