Domain-Driven Design (DDD) to podejście do tworzenia oprogramowania, które mówi: skup się na logice biznesowej, a nie tylko na technologii.
W praktyce oznacza to, że kod ma być odzwierciedleniem języka biznesu i procesów, które firma faktycznie realizuje.
DDD dzieli system na Bounded Contexts (BC), czyli wyraźnie oddzielone obszary odpowiedzialności.
👉 Przykład: w aplikacji e-commerce mamy Sprzedaż (zakupy, koszyk), Płatności (integracje z bramkami) i Magazyn (stany produktów). Każdy z tych działów jest innym światem, w którym obowiązują własne zasady i język.
No dobrze ale co zrobić, gdy te światy muszą ze sobą współpracować? Tu na scenę wchodzi Context Map.
Czym jest Context Map?
Context Map to mapa relacji pomiędzy różnymi Boounded Contextami (BC).
To trochę jak schemat metra, pokazuje, które linie (BC) się przecinają, kto jest zależny od kogo i jak się komunikują.
Mapy kontekstowe mogą być używane do analizy istniejących systemów lub środowisk aplikacji, ale nadają się również do wstępnego projektowania.
Kluczowe pojęcia
- Bounded Context to wydzielony obszar systemu z własnym językiem i modelem (np. moduł płatności).
- Upstream / Downstream to relacja kto dostarcza dane (upstream) i kto z nich korzysta (downstream).
- Integracja to sposób, w jaki dwa konteksty wymieniają dane (np. API, eventy, współdzielona baza która jest akurat antywzorcem).
Wzorce w Context Map

Shared Kernel
Shared Kernel to sytuacja, w której dwa (lub więcej) Bounded Contexty współdzielą część kodu np. klasy wartości, reguły walidacji czy wspólne obiekty domenowe.
To coś w rodzaju mini-wspólnej domeny, która jest tak fundamentalna, że jej duplikowanie prowadziłoby do problemów z niespójnością.
Przykład najczęstszy: Money, Currency, TaxRate, DateRange.
✅ Kiedy użyć
- Gdy fragment domeny jest stabilny i niezależny od szczegółów biznesowych czyli np. Money, ale niekoniecznie Order.
- Gdy nie możesz dopuścić do rozjazdu znaczeń np. VAT liczony w Sprzedaży i VAT liczony w Fakturowaniu musi być ten sam.
- Gdy zespoły są w stanie dogadać się co do odpowiedzialności za kod ponieważ Shared Kernel oznacza współodpowiedzialność.
⚠️ Ryzyka i pułapki
- Ukryte sprzężenie: jeśli w Shared Kernel wrzucisz za dużo, to nagle pół firmy jest od siebie zależne. Każda zmiana robi trzęsienie ziemi.
- Brak właściciela: kto decyduje, że w Money dodajemy nową metodę
roundUp()? Jeśli brak ustaleń, robi się chaos. - Eskalacja: Shared Kernel zaczyna od
Money.php, a kończy jako gigantyczna wspólna biblioteka, której nikt nie ogarnia.
👉 Dlatego w praktyce Shared Kernel powinien być minimalny. To nie wspólny kod wszystkich, tylko maleńki, dobrze przemyślany fragment domeny.
💡 Przykład biznesowy
Firma e-commerce:
- Sprzedaż liczy ceny koszyka, rabaty, koszty dostawy.
- Fakturowanie wystawia dokumenty księgowe, musi wyliczyć VAT.
Jeśli oba moduły mają własne definicje Money, to:
- w Sprzedaży ktoś doda obsługę groszy w integerach,
- w Fakturowaniu ktoś inny zrobi
float.
Efekt: rozjazdy w obliczeniach, trudne do debugowania błędy, różne interpretacje ceny.
Lepiej: oba moduły korzystają z jednego Shared Kernel:
final class Money
{
public function __construct(
private int $amount,
private string $currency
) {}
public function add(Money $other): Money
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException("Currencies don't match");
}
return new Money($this->amount + $other->amount, $this->currency);
}
public function format(): string
{
return number_format($this->amount / 100, 2) . ' ' . $this->currency;
}
}
Oba zespoły (Sprzedaż i Fakturowanie) zaufają temu samemu kontraktowi i dzięki temu mają spójność.
🎯 Zasady praktyczne dla Shared Kernel
- Minimalizm: trzymaj tam tylko najbardziej fundamentalne obiekty (Value Objects).
- Wspólna odpowiedzialność: zespoły muszą ustalić, kto zatwierdza zmiany.
- Stabilność: jeśli coś wciąż się zmienia, nie nadaje się do Shared Kernel.
- Testy kontraktowe: żeby każdy kontekst wiedział, że wspólny fragment działa zgodnie z oczekiwaniami.
Customer / Supplier
Customer/ Supplier to relacja, w której jeden kontekst dostarcza dane (Supplier), a drugi z nich korzysta (Customer). Customer ma pewien wpływ na Supplier i może zgłaszać wymagania, a Supplier bierze je pod uwagę. To relacja „zamawiający → dostawca”.
✅ Kiedy użyć
- Gdy Customer ma wpływ na Supplier (np. może zgłaszać wymagania).
- Gdy zespoły współpracują hierarchicznie (np. Product Team → Data Team).
⚠️ Ryzyka i pułapki
- Supplier może ignorować potrzeby Customer i wtedy relacja zamienia się w Conformist.
- Customer zaczyna wymuszać za dużo i Supplier staje się hamulcem rozwoju.
💡 Przykład biznesowy
Raportowanie (Customer) korzysta z Sprzedaży (Supplier). Zespół raportowania mówi: “Potrzebujemy nowych pól w API zamówień”. Zespół Sprzedaży je dodaje.
// Supplier: SalesApiClient
class SalesApiClient
{
public function getOrders(): array
{
return [
['id' => 1, 'total' => 12000, 'customer' => 'Jan'],
['id' => 2, 'total' => 4500, 'customer' => 'Anna'],
];
}
}
// Customer: Reporting
class ReportService
{
public function __construct(
private SalesApiClient $sales
) {}
public function generateReport(): array
{
return array_map(fn ($o) => [
'OrderId' => $o['id'],
'Amount' => $o['total'],
], $this->sales->getOrders());
}
}🎯 Zasady praktyczne dla Shared Kernel
- Customer ma głos → Supplier powinien słuchać.
- Warto mieć backlog integracyjny wspólny dla obu zespołów.
- Pilnować balansu, Supplier nie jest fabryką feature’ów dla Customer.
Conformist
Pattern Conformist to gdy downstream (odbiorca) musi przyjąć model upstream dokładnie taki, jaki jest. Bez negocjacji.
✅ Kiedy użyć
- Gdy upstream jest zbyt silny i nie ma negocjacji np. system globalny
- Gdy integrujesz się z dużym zewnętrznym systemem (np. API), którego nie zmienisz.
⚠️ Ryzyka i pułapki
- Twój model staje się zakładnikiem upstreamu.
- Kod zaczyna mówić językiem cudzej domeny, a nie Twojej.
💡 Przykład biznesowy
Integracja z PayPalApi i musisz korzystać z ich struktury danych.
class PaypalApi
{
public function getPayment(string $id): array
{
return ['paymentId' => $id, 'amount' => 100.5, 'currency' => 'USD'];
}
}
class PaymentService // conformist
{
public function __construct(
private PaypalApi $paypal
) {}
public function capture(string $paymentId): array
{
return $this->paypal->getPayment($paymentId);
}
}🎯 Zasady praktyczne
- Conformist akceptujesz tam, gdzie nie masz wyboru.
- Trzymaj kod związany z conformist w izolacji (np. adaptery).
- Jeśli możesz to zmień na ACL (poniżej opis).
Anti-Corruption Layer (ACL)
Tworzysz warstwę tłumaczącą cudzy model na własny, żeby nie zanieczyścił Twojej domeny.
✅ Kiedy użyć
- Gdy Twój kontekst ma inne pojęcia niż upstream.
- Gdy chcesz chronić swój kod przed “zanieczyszczeniem” cudzą terminologią.
⚠️ Ryzyka i pułapki
- ACL to dodatkowa warstwa → koszt wdrożenia i utrzymania.
- Zła implementacja = tylko “kopiuj wklej”, a nie prawdziwa translacja.
💡 Przykład biznesowy
System Kadry mówi Employee, system Payroll mówi Contractor. Tworzysz ACL, który mapuje Employee → Contractor.
class PayrollApi
{
public function getContractor(string $id): array
{
return ['contractorId' => $id, 'name' => 'Jan', 'rate' => 100];
}
}
class EmployeeDTO
{
public function __construct(
public string $id,
public string $fullName,
public float $hourlyRate
) {}
}
class PayrollTranslator
{
public static function toEmployee(array $data): EmployeeDTO
{
return new EmployeeDTO(
id: $data['contractorId'],
fullName: $data['name'],
hourlyRate: $data['rate']
);
}
}🎯 Zasady praktyczne
- ACL to ochrona języka domeny.
- Zawsze twórz testy translacji.
- ACL najlepiej działa jako osobna warstwa (adaptery, translatory).
Open Host Service (OHS)
Kontekst udostępnia uniwersalny interfejs (API), którego inni mogą używać.
✅ Kiedy użyć
- Gdy chcesz, aby wiele innych systemów mogło łatwo korzystać z Twojego kontekstu.
- Gdy tworzysz platformę, z której korzystają inni (np. SaaS).
⚠️ Ryzyka i pułapki
- Zła dokumentacja = OHS nie działa.
- API jest publiczne → każda zmiana boli.
💡 Przykład biznesowy
Moduł Płatności wystawia REST API, z którego korzystają Sprzedaż i Rachunkowość.
#[Route('/api/payments', methods: ['POST'])]
public function createPayment(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
// logika...
return new JsonResponse(['status' => 'ok', 'id' => uniqid()]);
}🎯 Zasady praktyczne
- API = kontrakt → nie zmieniaj bez wersjonowania.
- Dokumentacja (OpenAPI, Swagger) to must have.
- Stabilność > elastyczność.
Published Language (PL)
Standardowy, uzgodniony format wymiany danych.
✅ Kiedy użyć
- Gdy kilka kontekstów musi współpracować i trzeba ustalić wspólny kontrakt.
- Gdy wiele zespołów integruje się z Twoim API.
- Gdy integrujesz kilka niezależnych zespołów.
⚠️ Ryzyka i pułapki
- Brak jasnej specyfikacji → każdy interpretuje inaczej.
- Zmiana Published Language = migracja wszystkich integracji.
💡 Przykład biznesowy
Firma ustala, że wszystkie eventy biznesowe (np. OrderPlaced) są w JSON Schema.
$orderPlaced = [
'event' => 'OrderPlaced',
'orderId' => 123,
'total' => 4599,
'currency' => 'PLN',
'customer' => 'Jan Kowalski'
];
echo json_encode($orderPlaced);🎯 Zasady praktyczne
- Published Language musi być publiczny i wspólny.
- Ustalaj w kontrakcie (np. JSON Schema).
- Event Storming + wspólna definicja języka = najlepszy start.
Separate Ways
Konteksty nie integrują się i idą własnymi drogami.
✅ Kiedy użyć
- Gdy integracja jest droższa niż brak integracji.
- Gdy obszary nie mają krytycznych zależności.
⚠️ Ryzyka i pułapki
- Możesz przeoczyć ukrytą zależność i mieć dane niespójne.
- Klient oczekuje integracji, a Ty jej nie masz.
💡 Przykład biznesowy
System Marketing i system HR działają niezależnie i nie ma sensu ich łączyć, bo to tylko komplikacja. Każdy system działa osobno.
🎯 Zasady praktyczne
- Separate Ways to też świadoma decyzja!
- Używaj, gdy integracja to overkill.
- Warto dokumentować powód braku integracji.
Partnership
Dwa konteksty współpracują ramię w ramię, dzieląc odpowiedzialność za integrację.
✅ Kiedy użyć
- Gdy zespoły mają wspólny cel biznesowy i duże zaufanie.
- Gdy nie da się jasno ustalić hierarchii Customer/Supplier.
- Gdy integracja jest krytyczna i musi być rozwijana razem.
⚠️ Ryzyka i pułapki
- Wymaga dużego zaufania i komunikacji.
- Jeśli zespoły przestaną współpracować → integracja się sypie.
💡 Przykład biznesowy
Marketplace i Płatności tu oba zespoły wspólnie projektują API, razem decydują o wersjach.
// Uzgodnione DTO wspólnie przez oba zespoły
class PaymentRequest
{
public function __construct(
public string $orderId,
public float $amount,
public string $currency
) {}
}🎯 Zasady praktyczne
- Partnership wymaga synchronizacji roadmap.
- Oba zespoły = wspólny właściciel kontraktu.
- Regularne sync meetingi i wspólny backlog.
Krótkie podsumowanie
- Shared Kernel to wspólny kod.
- Customer/Supplier jest relacją zależności z możliwością wpływu.
- Conformist tutaj musisz się dopasować.
- ACL, chronisz się tłumacząc cudzy model.
- OHS czyli wystawiasz API.
- Published Language to wspólny standard wymiany danych.
- Separate Ways to brak integracji.
- Partnership jest bliską współpracą.
Context Map to mapa relacji pomiędzy różnymi Bounded Contextami. Wzorce pomagają świadomie wybrać sposób integracji. W PHP/Symfony najczęściej spotkasz Conformist i ACL, a w złożonych systemach np Shared Kernel i OHS.
PRO TIPY
👉 Najlepszy sposób, żeby to zrozumieć, to narysować mapę swojego systemu i spróbować nazwać relacje.
👉 Jeśli chcesz zgłębić temat jeszcze bardziej i zobaczyć praktyczne materiały, zajrzyj do świetnego repozytorium DDD Crew Context Mapping: https://github.com/ddd-crew/context-mapping.

W podręczniku DDD Reference zawarto krótkie podsumowanie każdej definicji i wzorca z książki Erica Evansa z 2004 r., a także trzy wzorce, które nie znalazły się w oryginalnej książce, a które Eric uważa teraz za część swojej wiedzy na temat DDD. Do pobrania za free tutaj -> https://www.domainlanguage.com/ddd/reference/
Polecam 🙂