Kapitola 03 · Strategie · Context Mapping – 8 vztahů mezi Bounded Contexts

Context Mapping – 8 vztahů mezi Bounded Contexts

Bounded Context vám definuje hranici. Context Mapping vám definuje, co se na té hranici děje. Osm pojmenovaných vztahů, které popisují všechny způsoby, jak spolu BC komunikují – od těsné spolupráce po úmyslnou separaci.

Autor M. Katuščák
Doba čtení ≈ 28 min
Náročnost pokročilá
Publikováno · Aktualizováno ·
Obsah kapitoly

Strategický design v DDD má dvě stránky. Bounded Context definuje hranici jednoho modelu – co je uvnitř, co je venku, kde končí jeden Ubiquitous Language a začíná druhý. Detail viz kapitolu Základní koncepty – Bounded Contexts. Bounded Context však sám o sobě neřeší jednu podstatnou otázku: co se děje na té hranici, když dva kontexty potřebují spolupracovat.

Context Mapping je odpověď. Eric Evans ji představil 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 všech vztahů mezi nimi [1]. Vaughn Vernon v Implementing Domain-Driven Design (2013) tuto disciplínu rozvedl o praktickou implementaci v moderním stacku [2]. Následující sekce pokrývají všech osm pojmenovaných vztahů mezi BC, jejich kompromisy a způsob, jakým se realizují v Symfony 8 – typicky kombinací CQRS s Symfony Messenger, REST API a HTTP klientů.

03.01 Co je Context Map a proč ji nakreslit

Eric Evans popisuje Context Map takto: „Identify each model in play on the project and define its bounded context. […] Describe the points of contact between the models, outlining explicit translation for any communication and highlighting any sharing.[1] Volně přeloženo: pojmenuj všechny modely v projektu, ohranič je a popiš, jak se na hranicích potkávají. Context Map není UML diagram tříd. Je to organizační a politická mapa, která zachycuje, kdo s kým mluví, jakým jazykem a kdo rozhoduje, když se ten 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/D pro upstream/downstream).
  • Textová složka – krátký dokument popisující každý vztah: odpovědné týmy, kontrakt, frekvenci změn, eskalační kontakt. Tato část je důležitější než obrázek; obrázek zastarává rychleji, než se stačí aktualizovat.

Proč tu mapu nakreslit? Protože alternativou je implicitní vztahový graf: tým A ví, že volá tým B, ale nikdo nikde neřekl, jakým způsobem se to děje, kdo kontrakt vlastní a co se stane, když ho jednostranně změníme. Implicitní vztahy vedou k integračním bugům, plíživému sdílení modelů a v konečném důsledku k Big Ball of Mud (viz 03.12).

FIG. 03.1-A Context Map: 5 Bounded Contexts a všech 8 typů vztahů
Příkladová Context Map fiktivního e-shopu. Catalog a Pricing jsou v Partnership a sdílí Money VO (Shared Kernel). Catalog dodává data Orderingu (Customer/Supplier). Ordering chrání svůj model před legacy Billingem přes ACL. Identity nabízí Open Host Service nad Published Language. Marketing zůstává v Separate Ways.

03.02 Osm typů vztahů – přehled

Eric Evans katalogizoval osm pojmenovaných vztahů, kterými mohou Bounded Contexts spolu vyjít. Vaughn Vernon je v IDDD (2013) doplnil o nuance a kombinace, ale jádro pojmenování zůstalo. Tato osmičlenná taxonomie je užitečná ze dvou důvodů: (1) dává nám sdílený slovník („zde je to Customer/Supplier, ne Conformist“) a (2) zviditelňuje cenu vztahu – některé jsou dražší než jiné a volba mezi nimi je strategická.

Pozn.: Evansova kapitola 14 v Domain-Driven Design obsahuje ještě devátý vzor – Big Ball of Mud. Probíráme jej samostatně v sekci 03.12 Anti-vzor: Big Ball of Mud, protože nejde o cílový vztah, který by si někdo vědomě volil, ale o stav rozpadu, kterému se aktivně bráníme. Osm vztahů níže jsou tedy navržitelné volby; Big Ball of Mud je to, co se stane, když žádnou volbu neuděláte.

Vztah Symetrický? Coupling Použití Kdo o něm rozhoduje
Partnership Symetrický Vysoký Společné doménové cíle, společný release Oba týmy
Shared Kernel Symetrický Vysoký Sdílený codebase modul (VO, eventy) Oba týmy souhlasem
Customer/Supplier Asymetrický Střední Upstream poskytuje, downstream konzumuje Upstream rozhoduje, downstream prioritizuje
Conformist Asymetrický Střední Downstream přijímá upstream model 1:1 Vynucené (downstream nemá vliv)
Anti-Corruption Layer Asymetrický Nízký Downstream chrání svůj model před upstreamem Downstream rozhoduje
Open Host Service Asymetrický Nízký Upstream stabilizuje protokol pro mnoho konzumentů Upstream rozhoduje
Published Language Asymetrický Nízký Stabilizovaný formát zpráv (schema) Upstream + standardy
Separate Ways Žádný Žádná integrace, vědomá duplicita Strategické rozhodnutí

Tabulka ukazuje, že vztahy nejsou nezávislé varianty – některé se kombinují. Customer/Supplier typicky používá Open Host Service jako kanál a Published Language jako formát zpráv. Anti-Corruption Layer je technika, kterou downstream aplikuje, když je v Customer/Supplier nebo Conformist pozici vůči nevstřícnému (legacy) modelu.

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 se nasazují společným release procesem. Není to „náhodná spolupráce“ – Partnership je vědomé strategické rozhodnutí, že integrační náklady (synchronní porady, společný roadmap, časté merge konflikty) jsou nižší než cena, kterou by oba týmy zaplatily, kdyby pracovaly nezávisle.

Evans (2003, str. 354): „Where development failure in either of two contexts would result in delivery failure for both, forge a partnership between the teams in charge of the two contexts. Institute a process for coordinated planning of development and joint management of integration.

Příklad: Catalog BC + Pricing BC v early-stage startupu

Představme si fiktivní e-shop, kde tým Catalog vlastní produktové informace (název, popis, obrázky, kategorie) a tým Pricing vlastní cenotvorbu (základní cena, slevy, A/B test ceny pro různé segmenty). V early-stage fázi platí dvě věci najednou:

  • Catalog bez Pricingu je k ničemu – produktovou stránku nelze zobrazit bez ceny.
  • Pricing bez Catalogu je k ničemu – cena bez produktu nedává smysl.

V této fázi se týmy záměrně rozhodnou pro Partnership: jeden produktový manažer pokrývá oba BC, retrospektivy se konají společně, release proces je jednotný (deploy obou BC současně). To je optimální, dokud se domény neusadí natolik, aby mohly žít vlastním tempem.

Symfony detail: monorepo a společný release

plaintext Monorepo struktura
app/
  src/
    Catalog/         ← Catalog BC
      Domain/
      Application/
      Infrastructure/
    Pricing/         ← Pricing BC (Partnership)
      Domain/
      Application/
      Infrastructure/
  config/
    services.yaml    ← sdílená DI registrace
    messenger.yaml   ← sdílená message bus konfigurace
composer.json        ← jeden composer.json pro oba BC

V tomto uspořádání mají oba BC vlastní namespacy (App\Catalog, App\Pricing) i vlastní invarianty, ale sdílí infrastrukturu (DI kontejner, RabbitMQ, databázový server). Komunikace mezi nimi je in-process přes Symfony Messenger sync transport – žádné serializované JSON přes drát.

Anti-vzor: „Partnership jako default“

Když se týmy rozhodnou pro Partnership, aniž by si tu otázku položily – typicky proto, že „nemáme čas se domluvit, takže to budeme dělat dohromady“ – vede to přímo k Big Ball of Mud. Agregáty jednoho BC začnou číst tabulky druhého BC „protože je to rychlejší“, doménové eventy se přestanou používat ve prospěch sdílených service tříd, a 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í odložit své vlastní featury, aby čekaly na druhý tým – celková rychlost klesá.
  • Retrospektivy se opakovaně točí kolem stejných „mezi-týmových“ napětí.
  • Release proces se prodlužuje, protože koordinace dvou roadmap je příliš nákladná.
  • Jeden tým získá silně odlišnou prioritu (např. Catalog SEO sprint, zatímco Pricing dělá compliance) – společné nasazení přestává mít smysl.

V tu chvíli je čas Partnership rozpustit a přejít na Customer/Supplier nebo Open Host Service mezi oběma BC.

03.04 Shared Kernel

Shared Kernel je malý modul kódu fyzicky sdílený mezi dvěma a více Bounded Contexts. Sdílení je oboustranně závazné: žádný vlastník nemůže Shared Kernel jednostranně změnit, protože to by porušilo invarianty v ostatních BC. Změna SK vyžaduje souhlas všech vlastníků, což je nákladný proces a důvod, proč musí SK zůstat malý.

Evans (2003, str. 355): „Designate with an explicit boundary some subset of the domain model that the teams agree to share. […] This kernel will be smaller than the natural intersection. Within this boundary, include, along with this subset of the model, the subset of code or of the database design associated with that part of the model. This explicitly shared stuff has special status, and shouldn't be changed without consultation with the other team.

Kdy Shared Kernel zvolit

  • Existuje koncept, který má v obou BC identický význam (typicky elementární VO: Money, Currency, EmailAddress, UserId).
  • Konceptů je málo (řekněme < 10 tříd) a jsou stabilní (mění se jednou ročně, ne týdně).
  • Týmy mají dobrou komunikaci a souhlasí, že koordinaci budou dělat.

Code sample: SharedKernel\Money modul

php shared-kernel/src/Money/Money.php
<?php

declare(strict_types=1);

// shared-kernel/src/Money/Money.php
namespace App\SharedKernel\Money;

use InvalidArgumentException;

final readonly class Money
{
    public function __construct(
        public int $amountCents,
        public Currency $currency,
    ) {
        if ($amountCents < 0) {
            throw new InvalidArgumentException(
                'Money cannot be negative; use SignedMoney for credits/debits.'
            );
        }
    }

    public function add(self $other): self
    {
        if (!$this->currency->equals($other->currency)) {
            throw new InvalidArgumentException(
                "Cannot add {$this->currency->code} and {$other->currency->code}."
            );
        }
        return new self($this->amountCents + $other->amountCents, $this->currency);
    }

    public function multiply(int $factor): self
    {
        return new self($this->amountCents * $factor, $this->currency);
    }

    public function equals(self $other): bool
    {
        return $this->amountCents === $other->amountCents
            && $this->currency->equals($other->currency);
    }
}

Money je VO bez identity – ideální kandidát na SK. Nemá závislosti na žádné infrastruktuře, je readonly, neměnný, plně testovatelný. Catalog ho používá pro vyjádření základní ceny, Pricing pro vyjádření slevy, Ordering pro celkovou částku objednávky – a ve všech BC znamená přesně totéž.

Symfony detail: composer path repository

json composer.json
{
    "name": "ddd/eshop",
    "type": "project",
    "repositories": [
        {
            "type": "path",
            "url": "shared-kernel/",
            "options": { "symlink": true }
        }
    ],
    "require": {
        "ddd/shared-kernel": "*"
    }
}

Shared Kernel se v Symfony monorepu typicky drží jako lokální composer balíček v adresáři shared-kernel/. Verzování probíhá přes git tagy (v1.0.0, v1.1.0) a změny SK procházejí společným code review obou (či více) týmů. Pull request do SK má v ideálním případě CODEOWNERS pravidlo, které automaticky přiřadí review obou týmů.

Anti-vzor: „rozjetý“ Shared Kernel

Nejčastější selhání Shared Kernelu je jeho růst. Tým si řekne: „máme tu Money, přidáme Address – vždyť adresa je taky všude stejná“. Pak PhoneNumber, pak Customer, pak Order… a najednou má SK 200 tříd a každá změna trvá týdny, protože vyžaduje souhlas tří týmů. V tu chvíli SK přestal být kernel a stal se Big Ball of Shared Mud.

Pravidlo: SK musí být malý, neměnný a recenzovaný oběma týmy. Pokud roste, znamená to, že koncepty, které do něj přidáváme, jsou v jednotlivých BC ve skutečnosti odlišné (jen vypadají podobně) a měly by být modelovány samostatně. Pokud koncept opravdu patří do SK ale je velký, je třeba ho vyčlenit do vlastního BC s Open Host Service.

03.05 Customer / Supplier

Customer/Supplier je asymetrický vztah, ve kterém upstream (supplier) poskytuje data nebo službu a downstream (customer) je konzumuje. Hlavní rozdíl proti Conformist (viz dále): downstream má hlas. Může od supplieru explicitně požadovat featury, supplier je do svého backlogu přijme a dohodne se na termínu. Supplier ale rozhoduje, kdy a jak feature dodá.

Evans (2003, str. 357): „The freewheeling development of the upstream team can be cramped if the downstream team has veto power over changes, or if procedures for requesting changes are too cumbersome. […] Establish a clear customer/supplier relationship between the two teams. In planning sessions, make the downstream team play the customer role to the upstream team. Negotiate and budget tasks for downstream requirements so that everyone understands the commitment and schedule.

Příklad: Catalog (supplier) → Ordering (customer)

Ordering BC potřebuje znát produktové ID a aktuální cenu, aby mohl sestavit objednávku. Catalog je vlastníkem produktových dat. Vztah je čistě jednosměrný: Ordering čerpá z Catalogu, Catalog bere Ordering jako „prvotřídního zákazníka“, ale neztrácí 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 dává smysl pro Catalog model (ano – Stock patří do Catalogu), naplánuje to do následujícího sprintu a dodá. Pokud by to nedávalo smysl pro Catalog model, navrhl by alternativu (např. samostatný Inventory BC s vlastním API).

Code sample: Symfony Messenger external transport

Customer/Supplier vztah se v Symfony 8 typicky implementuje přes asynchronní eventy. Catalog publikuje ProductPriceChanged do AMQP exchange, Ordering ho konzumuje přes external Messenger transport.

yaml config/packages/messenger.yaml (Ordering BC)
# config/packages/messenger.yaml – downstream Ordering BC
framework:
    messenger:
        transports:
            from_catalog:
                dsn: '%env(CATALOG_AMQP_DSN)%'
                options:
                    exchange:
                        name: 'catalog.events'
                        type: 'topic'
                    queues:
                        ordering.from_catalog:
                            binding_keys: ['product.price_changed', 'product.discontinued']
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 2
        routing:
            'App\Ordering\Application\ExternalEvent\ProductPriceChanged': from_catalog
            'App\Ordering\Application\ExternalEvent\ProductDiscontinued': from_catalog

A handler v Orderingu:

php src/Ordering/Application/EventHandler/ProductPriceChangedHandler.php
<?php

declare(strict_types=1);

namespace App\Ordering\Application\EventHandler;

use App\Ordering\Application\ExternalEvent\ProductPriceChanged;
use App\Ordering\Domain\PriceCache;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class ProductPriceChangedHandler
{
    public function __construct(
        private readonly PriceCache $priceCache,
    ) {}

    public function __invoke(ProductPriceChanged $event): void
    {
        // Ordering si drží lokální projekci ceny – eventually consistent.
        $this->priceCache->update(
            productId: $event->productId,
            newPrice: $event->newPriceCents,
            currency: $event->currency,
            effectiveAt: $event->occurredAt,
        );
    }
}

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 Customer/Supplier kombinuje s Open Host Service jako kanálem a Published Language jako formátem zpráv. Detail viz sekci 03.08 a 03.09.

Plánovací rituály mezi týmy

Customer/Supplier funguje jen při existenci minimálních koordinačních rituálů:

  • Pravidelný cross-team grooming (typicky 1× za sprint), kde downstream prezentuje své požadavky.
  • Dokumentovaný roadmap upstreamu – downstream musí vidět, co se chystá a kdy očekávat breaking changes.
  • Eskalační kanál – kdo rozhoduje, když se týmy neshodnou? Typicky produktový manažer nebo architekt.

Bez těchto rituálů sklouzne Customer/Supplier do Conformistu: downstream přestane mít hlas, jen se přizpůsobuje.

03.06 Conformist

Conformist je asymetrický vztah, ve kterém downstream vědomě rezignuje na vlastní model a přijímá upstream model 1:1. Žádný překlad, žádná validace, žádné mapování. Conformist není ostuda – je to strategické rozhodnutí, že boj o vlastní model nestojí za to.

Evans (2003, str. 360): „When two development teams have an upstream/downstream relationship in which the upstream has no motivation to provide for the downstream team's needs, the downstream team is helpless. […] Eliminate the complexity of translation between bounded contexts by slavishly adhering to the model of the upstream team. […] In some cases, the upstream design is good or compatible enough that this won't cause much trouble.

Kdy Conformist zvolit

  • Externí dodavatel – používáte SaaS (Stripe, Shopify, Auth0) a nemá smysl bojovat proti jejich datovým modelům.
  • 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 nemá smysl investovat do ACL.

Příklad: Reporting BC přijímá Stripe payment objekty 1:1

php src/Reporting/Application/StripePaymentReportRepository.php
<?php

declare(strict_types=1);

namespace App\Reporting\Application;

// Conformist: žádné vlastní VO, používáme přímo Stripe SDK objekty
use Stripe\PaymentIntent;
use Stripe\Charge;

final class StripePaymentReportRepository
{
    public function __construct(
        private readonly \Stripe\StripeClient $stripe,
    ) {}

    /**
     * @return PaymentIntent[]
     */
    public function getRecentPayments(\DateTimeImmutable $since): array
    {
        return $this->stripe->paymentIntents->all([
            'created' => ['gte' => $since->getTimestamp()],
            'limit'   => 100,
        ])->data;
    }

    public function generateMonthlyRevenue(int $year, int $month): array
    {
        $payments = $this->getRecentPayments(/* ... */);

        return array_map(
            // Reporting prostě používá Stripe pole jak jsou – currency, amount,
            // status. Žádný překlad na Money VO, žádná Czech terminologie.
            fn(PaymentIntent $p) => [
                'id'       => $p->id,
                'amount'   => $p->amount,        // Stripe používá centy
                'currency' => $p->currency,      // 'usd', 'eur' lower-case
                'status'   => $p->status,
            ],
            $payments,
        );
    }
}

Reporting je vědomě Conformist vůči Stripe. Žádný převod na Money VO, žádný překlad 'usd'Currency::USD. Když Stripe přejmenuje pole nebo přidá nový status, Reporting musí změnu přijmout. Cena za to je nulová investice do ACL; cena, kterou platíme, je křehkost vůči neslučitelným změnám upstreamu.

Trade-off 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, což zhoršuje srozumitelnost.
  • Pokud se upstream rozhodne odejít (Stripe zavře službu), je downstream odkázán na vendora.
  • Není možné sdílet model napříč více upstreamy (např. přidat alternativu PayPal vedle Stripe – celá doménová logika kopíruje Stripe).

Conformist jako přechodný stav

Často je Conformist přijatelný dočasně – projekt potřebuje rychle dodat MVP a integrace s upstreamem je třeba okamžitě. V tu chvíli je rozumné napsat Conformist, ale vypsat technický dluh do backlogu: „za 6 měsíců, až budeme vědět, jak Reporting používáme, postavíme ACL“. Bez explicitního zápisu do backlogu Conformist „uzraje“ na permanentní řešení a refactor 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, neupřímným) modelem. Překládá oběma směry, validuje vstupní data a filtruje neplatné stavy ještě předtím, než dorazí do domény. ACL je nejčastěji používaný vztah – a nejčastěji špatně implementovaný.

Evans (2003, str. 365): „Translation layers can be simple, even elegant, when bridging well-designed bounded contexts with cooperative teams. But when control or communication is not adequate to pull off a shared kernel, partnership, or customer-supplier relationship, translation becomes more complex. The translation layer takes on a more defensive tone. […] Therefore: As a downstream client, create an isolating layer to provide your system with functionality of the upstream system in terms of your own domain model.

FIG. 03.7-A Anatomie Anti-Corruption Layeru: tři odpovědnosti translátoru
ACL plní tři odpovědnosti najednou: schema mapping (DTO → VO), concept translation (legacy ID → doménová identita), anti-corruption (validace a odmítnutí neplatných zpráv).

Tři odpovědnosti ACL

  1. Schema mapping – překlad datových struktur. SOAP response, REST DTO, CSV řádek na doménová VO a entity. Zde se řeší „jak vypadá payload“.
  2. Concept translation – překlad významu dat. Upstream používá customerNumber jako int, my používáme CustomerId jako UUID. Upstream má status "PENDING", my máme enum OrderState::AwaitingPayment. Zde se řeší „co to znamená“.
  3. Anti-corruption – validace a filtrace. Negativní částky, chybějící required pole, status mimo známý enum, datum v budoucnosti – všechno musí ACL odmítnout, než se to dostane do domény. Zde se řeší „je to důvěryhodné“.

Code sample: kompletní LegacyBillingTranslator

php src/Ordering/Infrastructure/Acl/LegacyBillingTranslator.php
<?php

declare(strict_types=1);

namespace App\Ordering\Infrastructure\Acl;

use App\Ordering\Domain\Event\InvoicePaidEvent;
use App\Ordering\Domain\ValueObject\InvoiceId;
use App\SharedKernel\Money\Currency;
use App\SharedKernel\Money\Money;
use Symfony\Component\Messenger\Exception\IncomingMessageRejected;

/**
 * ACL mezi legacy Billing systémem (SOAP) a Ordering BC.
 *
 * Tři odpovědnosti:
 *   1. Schema mapping  – InvoicePaidSoapResponse => InvoicePaidEvent
 *   2. Concept translation – invoiceNumber (int) => InvoiceId (UUID)
 *   3. Anti-corruption – odmítá invalid stavy z legacy
 */
final class LegacyBillingTranslator
{
    public function __construct(
        private readonly LegacyBillingClient $soap,
    ) {}

    public function translateInvoicePaid(InvoicePaidSoapResponse $r): InvoicePaidEvent
    {
        // (3) Anti-corruption: legacy umí poslat negativní amount jako "credit"
        if ($r->amountCents < 0) {
            throw new IncomingMessageRejected(
                'Negative amount from legacy; credits are not supported in Ordering BC.'
            );
        }

        // (3) Anti-corruption: chybějící identifikátor
        if ($r->invoiceNumber === '' || $r->invoiceNumber === null) {
            throw new IncomingMessageRejected('Missing invoiceNumber in legacy payload.');
        }

        // (3) Anti-corruption: legacy umí poslat status, kterému nerozumíme
        if ($r->status !== 'PAID') {
            throw new IncomingMessageRejected(
                "Unsupported legacy status '{$r->status}'; expected PAID."
            );
        }

        // (1) Schema mapping + (2) Concept translation
        return new InvoicePaidEvent(
            invoiceId: InvoiceId::fromLegacy($r->invoiceNumber),
            paidAt:    new \DateTimeImmutable($r->paidAtIso),
            amount:    Money::ofCents($r->amountCents, Currency::EUR),
        );
    }
}

Translator je jediná veřejná metoda, final třída, bez stavu. Žádný state, žádná cache, žádný side-effect. Vstup je upstream DTO, výstup je doménová událost. Toto je důvod, proč je ACL tak silný – a tak křehký, když ho implementujete jinak.

Test ACL: snadný a důležitý

php tests/Ordering/Infrastructure/Acl/LegacyBillingTranslatorTest.php
<?php

declare(strict_types=1);

namespace App\Tests\Ordering\Infrastructure\Acl;

use App\Ordering\Infrastructure\Acl\InvoicePaidSoapResponse;
use App\Ordering\Infrastructure\Acl\LegacyBillingClient;
use App\Ordering\Infrastructure\Acl\LegacyBillingTranslator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Exception\IncomingMessageRejected;

final class LegacyBillingTranslatorTest extends TestCase
{
    public function testTranslatesPaidInvoice(): void
    {
        $soap = $this->createMock(LegacyBillingClient::class);
        $translator = new LegacyBillingTranslator($soap);

        $event = $translator->translateInvoicePaid(new InvoicePaidSoapResponse(
            invoiceNumber: 'INV-2025-00042',
            amountCents:   12_345,
            status:        'PAID',
            paidAtIso:     '2025-04-29T10:00:00Z',
        ));

        $this->assertSame(12_345, $event->amount->amountCents);
        $this->assertSame('EUR', $event->amount->currency->code);
    }

    public function testRejectsNegativeAmount(): void
    {
        $translator = new LegacyBillingTranslator($this->createMock(LegacyBillingClient::class));

        $this->expectException(IncomingMessageRejected::class);
        $translator->translateInvoicePaid(new InvoicePaidSoapResponse(
            invoiceNumber: 'INV-1',
            amountCents:   -100,
            status:        'PAID',
            paidAtIso:     '2025-04-29T10:00:00Z',
        ));
    }

    public function testRejectsUnknownStatus(): void
    {
        $translator = new LegacyBillingTranslator($this->createMock(LegacyBillingClient::class));

        $this->expectException(IncomingMessageRejected::class);
        $translator->translateInvoicePaid(new InvoicePaidSoapResponse(
            invoiceNumber: 'INV-2',
            amountCents:   100,
            status:        'CANCELLED', // legacy občas takhle pošle
            paidAtIso:     '2025-04-29T10:00:00Z',
        ));
    }
}

Protože ACL nemá stav a má jediný entrypoint, testuje se velmi snadno: tabulka vstup → výstup, plus tabulka vstup → výjimka. Každý nový edge case z produkce se promítá jako nový test.

Anti-vzor: prosakující ACL

Nejčastější selhání ACL: cizí pojmy začnou prosakovat do domény. Symptomy:

  • Doménová třída Order má pole legacyInvoiceNumber: string.
  • Doménový event OrderShipped má v payloadu stripeChargeId.
  • 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 je single-purpose třída, ne layer s desítkami metod sdílejících state. Jeden upstream koncept = jeden translator. Výstup translátoru je vždy doménový VO/entity/event, nikdy raw DTO. Pokud translátor začíná obsahovat doménovou logiku, je to signál, že máte Application Service schovanou v ACL – vyčleňte ji.

ACL a Strangler Fig pattern

Anti-Corruption Layer je důležitý stavební prvek Strangler Fig patternu pro postupnou migraci z legacy. Detail viz kapitolu Migrace z CRUD do DDD. V Strangler Fig přístupu je každý nový BC obklopen ACL, dokud legacy nezmizí – a v tu chvíli ACL většinou také zmizí (nebo se zjednoduší na čistý translator bez anti-corruption logiky).

03.08 Open Host Service (OHS)

Open Host Service je vzor, kdy upstream otevřeně publikuje stabilní dokumentovaný protokol pro mnoho downstream konzumentů. Místo aby upstream udržoval N různých integračních smluv (jeden customer = jeden kontrakt), publikuje jeden veřejný protokol a downstreamy se k němu přizpůsobí.

Evans (2003, str. 366): „When a subsystem has to be integrated with many others, customizing a translator for each can bog down the team. […] Define a protocol that gives access to your subsystem as a set of services. Open the protocol so that all who need to integrate with you can use it. Enhance and expand the protocol to handle new integration requirements, except when a single team has idiosyncratic needs. Then, use a one-off translator to augment the protocol for that special case so that the shared protocol can stay simple and coherent.

Kdy zvolit OHS

  • 3+ downstream konzumentů. S 1 konzumentem je OHS overkill – stačí Customer/Supplier ad hoc.
  • Stabilní doména. Upstream model se mění zřídka, takže investice do veřejného protokolu má návratnost.
  • Otevřená integrace, kde konzument může být i třetí strana (partneři, white-label klienti, mobilní aplikace).

Implementace v Symfony 8

V Symfony 8 OHS typicky znamená jedno z:

  • REST API přes api-platform/core nebo vlastní controllery, popsané OpenAPI spec.
  • gRPC přes spiral/php-grpc nebo roadrunner-server/grpc, popsané .proto souborem.
  • Event stream publikovaný přes RabbitMQ / Kafka, popsaný JSON Schema (přechod k PL).

Code sample: minimální OHS endpoint s versioningem

php src/Catalog/Infrastructure/Http/OpenHostService/ProductController.php
<?php

declare(strict_types=1);

namespace App\Catalog\Infrastructure\Http\OpenHostService;

use App\Catalog\Application\Query\GetProductQuery;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Messenger\HandleTrait;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;

final class ProductController extends AbstractController
{
    use HandleTrait;

    public function __construct(MessageBusInterface $queryBus)
    {
        $this->messageBus = $queryBus;
    }

    /**
     * OHS v1 – stabilní kontrakt zveřejněný v OpenAPI.
     * Verze v URL ('/api/v1/...') je explicitní záruka, že se kontrakt nemění.
     */
    #[Route('/api/v1/products/{id}', name: 'api_v1_product_get', methods: ['GET'])]
    public function getProductV1(string $id): JsonResponse
    {
        $product = $this->handle(new GetProductQuery($id));

        return $this->json($product, context: ['groups' => ['ohs.v1']]);
    }

    /**
     * OHS v2 – přidává pole 'availableStock'. v1 zůstává funkční pro staré klienty.
     */
    #[Route('/api/v2/products/{id}', name: 'api_v2_product_get', methods: ['GET'])]
    public function getProductV2(string $id): JsonResponse
    {
        $product = $this->handle(new GetProductQuery($id));

        return $this->json($product, context: ['groups' => ['ohs.v1', 'ohs.v2']]);
    }
}

Důležité: v1 a v2 koexistují. Zveřejnění OHS v1 je závazek – jakmile nějaký downstream začne v1 používat, nesmíte ji rozbít. Bez explicitního versioningu to není OHS, jenom „REST endpoint s nedostatečnou disciplínou“.

Versioning strategie

Tři běžné přístupy k versioningu OHS:

  • URI versioning (/api/v1/...) – nejčitelnější, snadno cacheovatelné, doporučené pro veřejné API.
  • Header versioning (Accept: application/vnd.catalog.v2+json) – čistější URL, ale komplikovaná diagnostika a debugging.
  • Query parameter (?api-version=2) – flexibilní, ale bývá zneužívaný k „polo-versioningu“ (nikdy nezmizí v1).

Deprecation politika

OHS musí mít explicitní deprecation politiku. Příklad pro veřejné API:

  • Při zveřejnění nové majoritní verze (v3) se starší verze (v1) označí jako deprecated v dokumentaci.
  • Hlavička Deprecation: true a Sunset: Sat, 31 Dec 2025 23:59:59 GMT se posílá v každé odpovědi v1.
  • Minimálně 6 měsíců před odstraněním v1 jsou všichni známí klienti notifikováni.
  • Po odstranění v1 vrací 410 Gone s odkazem na migrační průvodce.

03.09 Published Language (PL)

Published Language je dobře dokumentovaný, formálně specifikovaný formát zpráv mezi Bounded Contexts, který je nezávislý na konkrétním programovacím jazyce, frameworku ani databázi. PL si může každý konzument přečíst, validovat proti němu a generovat z něj kód.

Evans (2003, str. 370): „Use a well-documented shared language that can express the necessary domain information as a common medium of communication, translating as necessary into and out of that language.“ Vernon (2013) zdůrazňuje, že Published Language není jen schema – je to ubiquitous language pro integraci: pojmenovává koncepty, jejich invarianty a sémantiku.

OHS vs. PL – kanál vs. formát

Vztah OHS a PL bývá matoucí. Hlavní 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í.“

Můžete mít OHS bez PL (REST endpoint vracející ad-hoc JSON) – a je to špatně, protože downstream nemá jak validovat. Můžete mít PL bez OHS (Avro schema na disku) – a je to taky špatně, protože 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 schema, oblíbené pro Kafka/gRPC.

Code sample: JSON Schema pro OrderPlaced event

json order-placed-v1.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/events/order-placed-v1.json",
  "title": "OrderPlaced",
  "description": "Doménová událost vyvolaná po úspěšném vytvoření objednávky v Ordering BC.",
  "type": "object",
  "required": ["eventId", "orderId", "customerId", "totalAmount", "currency", "occurredAt"],
  "properties": {
    "eventId": {
      "type": "string",
      "format": "uuid",
      "description": "Unikátní ID této instance eventu (pro deduplikaci na konzumentech)."
    },
    "orderId": {
      "type": "string",
      "format": "uuid",
      "description": "ID objednávky v Ordering BC."
    },
    "customerId": {
      "type": "string",
      "format": "uuid",
      "description": "ID zákazníka v Identity BC. Stabilní napříč BC."
    },
    "totalAmount": {
      "type": "integer",
      "minimum": 0,
      "description": "Celková částka v nejmenší jednotce měny (centech)."
    },
    "currency": {
      "type": "string",
      "pattern": "^[A-Z]{3}$",
      "description": "ISO 4217 kód měny (EUR, CZK, USD)."
    },
    "occurredAt": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 timestamp v UTC, kdy bylo event vyvoláno upstreamem."
    }
  },
  "additionalProperties": false
}

Toto schema je publikováno na URL https://example.com/events/order-placed-v1.json a slouží jako kanonický kontrakt. Každý producer i konzument může proti němu validovat. Když Ordering BC chce přidat nové pole (například shippingAddressId), publikuje order-placed-v2.json a oba schémata koexistují minimálně po dobu deprecation okna.

Validace proti schema v Symfony

php src/Ordering/Infrastructure/PublishedLanguage/OrderPlacedValidator.php
<?php

declare(strict_types=1);

namespace App\Ordering\Infrastructure\PublishedLanguage;

use Opis\JsonSchema\Errors\ErrorFormatter;
use Opis\JsonSchema\Validator;
use Symfony\Component\Messenger\Exception\IncomingMessageRejected;

final class OrderPlacedValidator
{
    public function __construct(
        private readonly Validator $validator,
    ) {}

    public function validate(string $json): void
    {
        $data = json_decode($json, false, flags: JSON_THROW_ON_ERROR);

        $result = $this->validator->validate(
            $data,
            'https://example.com/events/order-placed-v1.json',
        );

        if (!$result->isValid()) {
            $errors = (new ErrorFormatter())->format($result->error());
            throw new IncomingMessageRejected(
                'Payload does not match OrderPlaced v1 schema: ' . json_encode($errors),
            );
        }
    }
}

Schema validation je první krok ACL na konzumující straně. Pokud payload neprojde schema validací, vrací se IncomingMessageRejected a zpráva se přesouvá do dead letter queue. Bez schema 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. Přijímáme duplicitu dat nebo paralelní procesy místo integrace, protože integrační náklady by byly vyšší než hodnota integrace. Separate Ways je jediný „vztah“, který znamená „žádný vztah“.

Evans (2003, str. 364): „Integration is always expensive. Sometimes the benefit is small. Therefore: Declare a bounded context to have no connection to the others at all, allowing developers to find simple, specialized solutions within this small scope.

Příklad: Marketing BC posílá maily přes vlastní SendGrid

Představme si situaci: Identity BC má hlavní seznam zákazníků s preferencemi. Marketing BC posílá hromadné mailové kampaně. Mohli bychom je integrovat:

  • Identity by publikovala CustomerEmailChanged event a 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á: za rok rozešle 50 kampaní, integrace stojí 200 hodin vývoje + 8 hodin údržby měsíčně, a riziko špatně synchronizovaného opt-in stavu by stejně bylo nenulové. Místo toho přijme Separate Ways:

  • Marketing si drží vlastní mailing list v SendGridu.
  • Při odhlášení v hlavičce mailu (CAN-SPAM compliance) SendGrid zákazníka odhlásí lokálně.
  • Když se zákazník odhlásí v Identity, Marketing pořád může poslat kampaň, ale bude tam legal opt-out v patičce a SendGrid honoruje globální unsubscribe.

Není to ideální – mohou nastat krátká okna, kdy odhlášený zákazník dostane jednu kampaň navíc – ale je to levné a legal-compliant. A hlavně: je to vědomé rozhodnutí, ne opomenutí.

Kdy Separate Ways zvážit

  • Low-value integrace – synchronizovaná data nepřinesou výrazné UX zlepšení.
  • High-effort sync – integrace by vyžadovala distribuovaný konsensus, eventually consistent projekce, complex retry logiku.
  • Krátká životnost jednoho z BC – Marketing kampaňový engine 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 pro vyhnutí se práci. Pokud tým prohlásí „my to nebudeme integrovat, je to příliš složité“ bez vyčíslení nákladů a hodnoty, není to Separate Ways – je to skrytý technický dluh. Skutečný Separate Ways má dokumentovanou alternativu (jak jsme to vyřešili bez integrace) a dokumentovaný edge case (co se stane, když se to rozpadne).

03.11 Praktický postup – jak nakreslit Context Map za 90 minut

Context Map se nepíše v izolaci jedním architektem. Je to týmové cvičení, které vyžaduje účast lidí znalých všech BC v systému. Ideální velikost skupiny: 3–8 lidí. Ideální nástroj: tabule, sticky notes, fixy. Digitální nástroje (Miro, FigJam) jsou v pořádku, fyzická interakce u tabule však odhalí mezi týmy víc napětí než cokoli online.

Pět kroků workshopu

  1. (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“). Pokud někdo přidá víc než 12 BC, je to varovný signál – možná je modelujete příliš jemně.
  2. (15–45 min) Pro každou dvojici BC, která spolu interaguje, nakreslit šipku. Šipka = směr toku dat / kauzality. Pojmenovat vztah jedním z 8 typů. Pokud se tým neshodne („je to Customer/Supplier nebo Conformist?“), je to indikátor, že vztah je nedefinovaný a stojí za eskalaci. Označit žlutým fixem.
  3. (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.
  4. (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 / koncept ADR).
  5. (80–90 min) Zachytit výsledek. Vyfotit tabuli, vložit do docs/context-map.png v repu, doprovázet Markdown souborem docs/context-map.md s textovým popisem každého vztahu (kdo vlastní, kontrakt, frekvence změn, eskalační kontakt). Owner = architekt nebo tech lead.

Co dát do textového popisu vztahu

plaintext docs/context-map.md (fragment)
## Catalog -> Ordering (Customer/Supplier + OHS + PL)

- **Upstream tým**: @catalog-team
- **Downstream tým**: @ordering-team
- **Kanál**: AMQP topic `catalog.events`
- **Schema**: https://schemas.example.com/catalog/product-v2.json
- **Frekvence změn**: ~1× kvartál (minor), 1× rok (major)
- **Eskalační kontakt**: @lead-architect
- **SLA**: 99.9% delivery within 5s, dead letter queue po 3 retry
- **Onboarding**: docs/onboarding/consume-catalog-events.md

Verzování Context Mapy

Context Map je živý dokument. Doporučení:

  • Datum poslední aktualizace v patičce povinné.
  • Verzování přes git – Markdown a SVG/PNG v repu.
  • Revize 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ů, i když se nic „nestalo“ – často se ukáže, že něco se stalo a nikdo to nezdokumentoval.

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 [3]: „A Big Ball of Mud is a haphazardly structured, sprawling, sloppy, duct-tape and baling-wire, spaghetti-code jungle.“ V jazyce Context Mappingu: systém, kde každý BC „nějak“ mluví s každým, sdílí databázové tabulky, sdílí entity, sdílí ad-hoc service vrstvy – a nikdo nedokáže nakreslit Context Map, 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.
  • Cirkulární závislosti mezi BC – A volá B volá C volá A.
  • Doctrine entity sdílené napříč BC – jediná třída Order je používaná v Catalog, Pricing i Billing s odlišnými očekáváními.
  • Service tříd s 50+ veřejnými metodami, které „obstarají všechno“.
  • Žádný explicitní integrační kontrakt – komunikace přes přímé volání DB, sdílené Redis klíče, file system.
  • Nemožnost říci „kde končí jeden BC a začíná druhý“.

Proč k tomu dochází

Foote & Yoder upozorňují: Big Ball of Mud is the de-facto standard architecture. Vzniká přirozeně, když:

  • Tým je pod tlakem dodat funkčnost rychle a nemá čas přemýšlet o hranicích.
  • Onboarding nových inženýrů probíhá kopírováním existujících vzorů, které samy o sobě jsou špatné.
  • Architekt(i) chybí nebo jsou ignorováni.
  • Refactoring je politicky obtížný (přidává riziko ke krátkodobému dodání).

Cesta ven

Big Ball of Mud se nedá „opravit“ rewriteem. Jediný funkční postup je Strangler Fig: postupně vyčleňovat čisté BC s ACL (viz Migrace z CRUD do DDD), každý BC obklopit Anti-Corruption Layerem, postupně přesouvat funkčnost ze staré spaghetti vrstvy do nového čistého modelu.

Detail anti-vzorů a jejich projevů v Symfony 8 najdete v kapitole Anti-vzory v DDD. Tato kapitola pokrývá konkrétní symptomy v PHP/Symfony stacku a strategie jejich nápravy.

03.13 Shrnutí

Context Mapping je strategická disciplína, která dává smysl Bounded Contextům tím, že popisuje, co se na jejich hranicích děje. Hlavní body:

  • Context Map = mapa vztahů. Vizuální + textová dokumentace všech BC v systému a všech vazeb mezi nimi. Není to UML class diagram, je to organizační a politická mapa.
  • Osm pojmenovaných vztahů. Partnership, Shared Kernel, Customer/Supplier, Conformist, ACL, OHS, Published Language, Separate Ways. Každý má svůj kompromis (coupling vs. flexibilita) a každý odpovídá jiné organizační situaci.
  • ACL je nejčastěji potřebný vztah. Skoro každá netriviální integrace s legacy nebo externím systémem chce ACL. Tři odpovědnosti: schema mapping, concept translation, anti-corruption.
  • OHS + PL = stabilní veřejná integrace. Open Host Service je kanál, Published Language je formát. Bez versioningu nejde o OHS.
  • Big Ball of Mud = „ještě jsme nedělali Context Map“. Pokud nedokážete nakreslit Context Map, máte BBoM. Cesta ven začíná deskriptivní mapou současného stavu, ne kódem.

Pro praktické nakreslení Context Mapy doporučujeme techniku Event Stormingu jako discovery workshop – odhalí jak hranice BC, tak vztahy mezi nimi v jediném sezení. Pro propojení s organizačním designem viz Team Topologies – Conway's Law říká, že Context Map a org chart musí korespondovat, jinak jeden z nich vyhraje a ten druhý se rozsype.

Č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ě, plus plánovaná revize minimálně 1× za 6 měsíců. Pokud nemáte čas vizuální složku udržovat aktualně, ponechte si alespoň textový popis (docs/context-map.md) – ten 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, kterou downstream aplikuje uvnitř Customer/Supplier nebo Conformist vztahu. 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 je typicky implementován pomocí Adapterů, ale ne každý Adapter je ACL. ACL má navíc tři specifické odpovědnosti – schema mapping, concept translation a anti-corruption (filtraci) – které „obyčejný“ Adapter nemá. 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 – někdy je to vědomé strategické rozhodnutí, které tým zapomněl zdokumentovat. (2) Pokud je nechtěný, vyčíslete cenu jeho opravy: kolik domén 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 translator za sprintem. 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 – je to průběžně udržovaný stav, kdežto ADR popisuje konkrétní rozhodnutí v čase. Ale každá změna Context Mapy by 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) je textová Context Map v Markdownu zcela dostatečná. 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, snadná git review, snadný 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].
  • Vaughn Vernon, Implementing Domain-Driven Design, kap. 3 „Context Maps“ (Addison-Wesley, 2013) [2].
  • Brian Foote & Joseph Yoder, Big Ball of Mud, PLoP 1997 [3].
  • Martin Fowler, Bounded Context (bliki) [4].
  • Martin Fowler, Context Map (bliki) [5].
  • 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 [6] – moderní vizuální notace pro Context Mapping.