
Sprzedaż dodaje pole do wspólnego obiektu Customer. Rozliczenia potrzebują innego sposobu ustalania adresu, a magazyn zaczyna interpretować ten sam status zamówienia po swojemu. Każda zmiana wymaga uzgodnień między zespołami, chociaż na diagramie architektury wciąż widać kilka niezależnych serwisów.
Context Map pomaga zobaczyć, gdzie kończy się jeden model i jak wpływa na drugi. Pokazuje granice języka, zależności między kontekstami oraz sposób współpracy ich właścicieli. Dzięki temu można rozmawiać o koszcie zmian, zamiast ograniczać dyskusję do liczby usług i strzałek HTTP.
Granica kontekstu to granica znaczenia
Bounded Context wyznacza obszar, w którym pojęcia i reguły modelu mają spójne znaczenie. W sprzedaży „klient” może oznaczać kupującego z uzgodnionym cennikiem. W rozliczeniach ważny jest nabywca dokumentu, jego dane podatkowe oraz odpowiedzialność za płatność. W obsłudze użytkowników kluczowe będą konto i uprawnienia.
Te modele mogą odnosić się do tego samego podmiotu, korzystać ze wspólnego identyfikatora i zawierać część podobnych danych. Nie muszą jednak dzielić jednej klasy ani tego samego cyklu życia. Dane nabywcy na wystawionej fakturze mogą wymagać zachowania historycznej postaci, mimo że bieżący profil klienta został już poprawiony.
Różne znaczenia słowa są sygnałem do szukania granicy. Sprawdź również reguły, właścicieli decyzji, tempo zmian i potrzebę spójnego zapisu. Sama różnica nazwy w dwóch działach nie dowodzi jeszcze istnienia osobnych kontekstów. Martin Fowler opisuje ten problem w artykule Bounded Context.
Kontekst, subdomena i mikroserwis
Subdomena opisuje część problemu biznesowego, na przykład ustalanie cen lub realizację zamówień. Bounded Context jest granicą konkretnego modelu, którym rozwiązujesz ten problem. W istniejącym systemie ich podział nie musi pokrywać się idealnie; rozbieżności bywają ważnym wynikiem analizy.
Kontekst nie musi być osobnym procesem. Możesz zachować granice modeli w modułowym monolicie. Jeden kontekst może też wymagać kilku wdrażanych komponentów. Oddzielna baza lub repozytorium kodu może wzmacniać izolację, lecz sama nie ustanawia spójnego języka ani własności danych.
Wspólna fizyczna baza również nie przekreśla podziału, jeśli moduły mają jawne reguły dostępu. Bezpośredni zapis do cudzych tabel, współdzielone encje ORM i wspólne migracje są jednak zależnościami, które trzeba pokazać. Rozbicie jednego modelu na kilka usług sieciowych może tylko przenieść jego sprzężenie do komunikacji między procesami.
Przykład: obsługa zamówienia w sklepie
Załóżmy, że pracujemy nad sklepem, w którym sprzedaż, realizacja i rozliczenia rozwijają własne modele. Operator płatności jest zewnętrznym dostawcą. Poniższa mapa pokazuje wybrane zależności kontraktów, a nie pełny przebieg zakupu.
- Sprzedaż odpowiada za przyjęcie zamówienia i uzgodnione warunki handlowe. Jej
Orderzawiera pozycje, ceny i reguły złożenia. - Realizacja odpowiada za przygotowanie i wysyłkę. Potrzebuje zlecenia realizacji, pozycji i adresu dostawy, a nie całego agregatu sprzedażowego.
- Rozliczenia odpowiadają za dokumenty i ewidencję płatności. Mają własne znaczenie kwoty rozliczonej, nabywcy i statusu dokumentu.
- Operator płatności publikuje zewnętrzny kontrakt. Jego statusy i identyfikatory wymagają interpretacji po stronie rozliczeń.
Na potrzeby przykładu zakładamy, że złożenie zamówienia upoważnia do utworzenia zlecenia realizacji, a rozliczenia samodzielnie ustalają dalsze kroki. Jeśli w danym biznesie wysyłkę wolno rozpocząć dopiero po potwierdzeniu płatności, mapa kontraktów i proces muszą to uwzględnić. Reguły przykładu nie są uniwersalnym modelem sklepu.
Upstream i downstream opisują wpływ
Upstream kształtuje model lub kontrakt, od którego zależy downstream. Jeżeli sprzedaż definiuje OrderSubmittedV1, a realizacja musi obsłużyć jego zmianę, dla tego kontraktu sprzedaż jest upstream. Odbiorca nie przestaje być downstream tylko dlatego, że sam inicjuje wywołanie API.
To nie jest hierarchia ważności zespołów ani określenie kierunku wszystkich danych. Ten sam kontekst może być downstream w jednej relacji i upstream w innej. Rozliczenia dostosowują się do API operatora płatności, a jednocześnie mogą udostępniać własny kontrakt informacji o rozliczeniu zamówienia.
Ustal legendę przed rysowaniem. Na tej mapie strzałka biegnie U → D, czyli zgodnie z wpływem kontraktu na odbiorcę. W innych narzędziach strzałki mogą oznaczać zależność kodu albo przepływ wiadomości. Mieszanie tych znaczeń daje diagram, który każda osoba czyta inaczej.
Jak nazwać relację między kontekstami
Wzorce Context Mapping opisują różne aspekty współpracy. Część dotyczy organizacji pracy, część sposobu udostępniania modelu i ochrony odbiorcy. Nie trzeba wybierać jednej etykiety dla wszystkiego. Na jednej granicy mogą współistnieć na przykład Open Host Service, Published Language i ACL. Zestaw relacji i ich kombinacje przedstawia również dokumentacja Context Mapper.
Partnerstwo, wspólne jądro i wpływ na dostawcę
- Partnership: powodzenie obu kontekstów wymaga skoordynowanej pracy. Zespoły wspólnie planują integrację i reagują na problemy. Kosztem jest zależność harmonogramów oraz konieczność regularnych uzgodnień.
- Shared Kernel: strony świadomie współdzielą mały fragment modelu lub implementacji i uzgadniają jego zmiany. Potrzebne są jasny zakres, wspólne testy i zasady wydawania. Biblioteka wszystkich encji firmy zwykle przekracza taki cel.
- Customer/Supplier: downstream jest odbiorcą upstream i może wpływać na jego plan rozwoju. „Customer” oznacza tu stronę korzystającą z modelu, a nie klienta sklepu. Ustalacie potrzeby, terminy i sposób wprowadzania zmian.
W przykładzie realizacja negocjuje ze sprzedażą, jakich informacji potrzebuje w zleceniu. To uzasadnia relację Customer/Supplier. Sama zależność od API nie wystarcza: jeśli zespół odbiorcy nie ma wpływu na plan dostawcy, etykieta opisywałaby życzenie zamiast faktycznej współpracy.
Dostosowanie albo ochrona własnego modelu
- Conformist: downstream przyjmuje model upstream, ograniczając własne tłumaczenie. Może to być rozsądne dla prostego obszaru pomocniczego, gdy koszt własnego modelu przewyższa korzyści. Cena pojawia się przy zmianach dostawcy i niedopasowaniu jego pojęć.
- Anti-Corruption Layer, ACL: downstream tłumaczy obce pojęcia na własny model. Chroni reguły i język odbiorcy, ale wymaga utrzymania mapowania, obsługi błędów i testów kontraktu.
- Separate Ways: strony świadomie rezygnują z integracji. Niewielka duplikacja może kosztować mniej niż zależność. Trzeba jednak sprawdzić, czy brak wspólnych danych nie łamie wymagania biznesowego.
ACL nie jest automatycznie najlepszą odpowiedzią na każde zewnętrzne API. Warto ją stosować, gdy obcy model realnie zagraża znaczeniu własnych pojęć albo regułom. Jeśli integracja sprowadza się do prostej, stabilnej informacji, rozbudowana warstwa tłumaczenia może być zbędna.
Open Host Service i Published Language
Open Host Service, OHS oznacza udostępnienie usługi integracyjnej przez uzgodniony protokół, z której mogą korzystać odbiorcy. W naszym przykładzie odbiorcy korzystają z utrzymywanego interfejsu subskrypcji zdarzeń sprzedaży. „Open” nie oznacza anonimowego dostępu z internetu. Usługa nadal może wymagać uwierzytelnienia, autoryzacji i limitów użycia.
Published Language, PL jest opisanym językiem wymiany: strukturą i znaczeniem komunikatów, znanymi odbiorcom. Sam dokument OpenAPI lub JSON Schema nie wyjaśni wszystkich reguł. Dla OrderSubmittedV1 trzeba ustalić również znaczenie „złożone”, jednostkę kwoty, walutę, identyfikatory, wymagane pola oraz sposób zmiany wersji.
Na mapie sprzedaż udostępnia rozliczeniom utrzymywany kontrakt OHS/PL. Rozliczenia mogą przetłumaczyć go na własny model. Wspólny format komunikacji nie zobowiązuje obu kontekstów do współdzielenia wewnętrznych encji.
ACL w praktyce: status płatności ma znaczenie
Operator przekazuje status authorized. Czy to oznacza, że można oznaczyć fakturę jako opłaconą? Niekoniecznie: w tym przykładzie autoryzacja jest rezerwacją środków, a dopiero captured oznacza pobranie. Dokładne znaczenie trzeba sprawdzić w kontrakcie konkretnego dostawcy.
Prosty fragment tłumaczenia może wyglądać tak:
public enum PaymentState
{
AwaitingFunds,
Authorized,
Captured,
Rejected
}
public static PaymentState TranslateProviderStatus(
string providerStatus) => providerStatus switch
{
"pending" => PaymentState.AwaitingFunds,
"authorized" => PaymentState.Authorized,
"captured" => PaymentState.Captured,
"failed" => PaymentState.Rejected,
_ => throw new UnsupportedProviderStatusException(
providerStatus)
};
Nieznany status nie powinien po cichu stawać się sukcesem. Trzeba zachować możliwość analizy, zaalarmować o nieobsługiwanym wariancie i zapewnić ścieżkę ponownego przetworzenia lub uzgodnienia stanu. Sam wyjątek bez takiej obsługi może tylko zablokować kolejkę.
Pełna ACL obejmuje więcej niż zmianę nazw pól. Weryfikuje znaczenie kwot i walut, mapuje identyfikatory oraz rozstrzyga niezgodne cykle życia. Warstwa wejściowa dodatkowo sprawdza autentyczność komunikatu. Reguły przejścia własnego obiektu ze stanu do stanu pozostają w modelu rozliczeń; status dostawcy nie powinien bezwarunkowo nadpisywać ich publicznym setterem.
Taki kod można utrzymać wewnątrz modułu, bez osobnego serwisu. Wzorzec i jego koszty omawia dokumentacja Anti-Corruption Layer. Sama nazwa katalogu Adapters nie dowodzi jeszcze, że obcy model został odizolowany.
Co zapisać przy każdej granicy
Do linii na diagramie dopisz krótką kartę relacji. Dla sprzedaży i realizacji mogłaby zawierać:
- Cel: utworzenie zlecenia realizacji po przyjęciu zamówienia.
- Właściciele: zespół sprzedaży odpowiada za kontrakt zdarzenia, zespół realizacji za jego interpretację i własne zlecenie.
- Tożsamość:
OrderIdpozwala skorelować modele; zlecenie realizacji może mieć dodatkowo własny identyfikator. - Dane i semantyka: pozycje oraz adres są obrazem uzgodnionego zamówienia. Późniejsza zmiana profilu klienta nie zmienia automatycznie adresu tego zlecenia.
- Czas i awarie: określacie dopuszczalne opóźnienie, obsługę duplikatów, anulowanie i zachowanie po braku odpowiedzi.
- Zmiana kontraktu: ustalacie kompatybilność, czas migracji odbiorców i osobę zatwierdzającą zmianę znaczenia.
Mapa nie zastąpi projektu komunikacji. Dla asynchronicznej integracji potrzebne mogą być outbox, idempotencja i kontrola kolejności opisane w artykule Outbox Pattern. Dla synchronicznego API trzeba określić limity czasu, błędy i zależność dostępności. Każdy wariant powinien realizować tę samą uzgodnioną odpowiedzialność biznesową.
Nie zakładaj też wspólnej transakcji dlatego, że dwa modele opisują to samo zamówienie. Jeśli proces wymaga atomowej zmiany, trzeba sprawdzić zasadność granicy albo zaprojektować koordynację i obsługę częściowego powodzenia. Szerzej opisuje to artykuł Kiedy Aggregate Root to za mało.
Jak poprowadzić warsztat nad istniejącym systemem
Nie zaczynaj od rysowania docelowych mikroserwisów. Wybierz jeden proces, na przykład złożenie, opłacenie i wysłanie zamówienia. Zaproś osoby, które znają reguły oraz rzeczywiste zależności: przedstawicieli biznesu, zespoły rozwijające obszary i osoby utrzymujące integracje.
- Odtwórz przebieg procesu. Zapisz decyzje, zdarzenia i wyjątki. Sprawdź, kto może zmienić poszczególne dane i gdzie rozstrzygane są reguły.
- Zbierz słowa, które zmieniają znaczenie. Porównaj „klienta”, „zamówienie”, „opłacone” i „anulowane” między obszarami. Zapisz definicje i przykłady.
- Narysuj obecną mapę. Uwzględnij wspólne biblioteki, bezpośredni dostęp do tabel, ręczne eksporty oraz uzgodnienia między zespołami.
- Nazwij relacje i sprawdź ich prawdziwość. Kto wpływa na czyj plan? Kto musi dostosować kod po zmianie kontraktu? Czy wspólne jądro ma ustalonego właściciela?
- Zaznacz niepewności i ryzyka. Brak wiedzy o właścicielu pola jest wynikiem warsztatu, a nie powodem, żeby narysować domyślnego właściciela.
- Wybierz jedną zmianę. Może to być wydzielenie modelu odbiorcy, jawna umowa kontraktu albo ograniczenie zapisu do cudzej tabeli. Ustal właściciela i sposób oceny efektu.
Oddziel mapę obecną od docelowej i oznacz datę oraz zakres każdej z nich. EventStorming może pomóc zebrać zdarzenia i reguły procesu, ale same kolorowe karteczki nie rozstrzygają granic. Kandydatów trzeba sprawdzić w kodzie, danych i sposobie pracy zespołów. Szerszy przebieg analizy opisuje Domain Analysis w Azure Architecture Center.
Co sprawia, że mapa przestaje pomagać
- Kontekst dla każdej tabeli. Tabele opisują przechowywanie danych; reguły i język mogą obejmować wiele z nich.
- Kontekst dla każdego działu lub repozytorium. Struktura organizacji i kodu daje wskazówki, lecz nie zastępuje analizy modelu.
- Wspólny model na wszystkie potrzeby. Pakiet współdzielonych encji może wymuszać jednoczesne zmiany i wydania, mimo oddzielnych wdrożeń.
- ACL ograniczona do mechanicznego mapowania JSON-a. Zmiana nazw pól nie rozwiązuje różnic w regułach, czasie i znaczeniu statusów.
- Mapa bez właścicieli i legendy. Strzałka nie wyjaśni, kto utrzymuje kontrakt ani co zrobić po jego zmianie.
- Docelowy obraz podpisany jako stan obecny. Pominięte zależności wrócą podczas pierwszej próby niezależnego wdrożenia.
Nie oceniaj jakości mapy liczbą kontekstów. Przydatny podział pozwala powiedzieć, gdzie zmienić regułę, kto podejmuje decyzję i jakie integracje trzeba uzgodnić. Jeśli każda drobna zmiana wymaga modyfikacji wszystkich modeli, sprawdź zarówno granice, jak i zakres współdzielenia.
Jak utrzymać mapę przy życiu
Przechowuj mapę blisko dokumentacji systemu, z datą, zakresem i wskazanymi właścicielami. Odnośniki do kontraktów, decyzji architektonicznych oraz kart relacji pomagają przejść od ogólnego obrazu do konkretnego ustalenia. Aktualizuj ją przy zmianie odpowiedzialności, sposobu integracji albo wpływu między zespołami.
Przy następnym review zapytaj: czy nowa funkcja wprowadza obce pojęcie do modelu, kto staje się właścicielem danych i czy zmienia się kontrakt na granicy? Takie pytania są praktycznym testem użyteczności Context Map.
Zacznij od jednego procesu i uczciwego opisu obecnych zależności. Dobra mapa daje wspólny język do podejmowania decyzji oraz pokazuje, gdzie potrzebna jest rozmowa. Jej wartość ujawnia się wtedy, gdy pomaga przeprowadzić kolejną zmianę bez przypadkowego naruszania cudzych reguł.