Domain-Driven Design (DDD) to podejście do tworzenia oprogramowania, które stawia na pierwszym miejscu głębokie zrozumienie domeny biznesowej, a dopiero później detale techniczne.
- Zrozumienie Domain-Driven Design – definicja, pochodzenie i główna filozofia
- Projektowanie strategiczne – ustanawianie struktury domeny poprzez bounded contexts i ubiquitous language
- Ubiquitous language – podstawa wspólnego zrozumienia
- Bounded contexts – tworzenie klarownych granic organizacyjnych
- Subdomeny – kategoryzowanie złożoności domeny
- Wzorce projektowania taktycznego – podstawowe elementy modeli domenowych
- Encje i obiekty wartości – rozróżnianie obiektów domeny
- Agregaty i korzenie agregatów – granice spójności
- Usługi domenowe – kapsułkowanie operacji, które nie pasują do obiektów
- Repozytoria – abstrakcja warstwy trwałości
- Fabryki i tworzenie obiektów wartości
- Zdarzenia domenowe – rejestrowanie zmian stanu i efektów ubocznych
- Event storming i współdzielone modelowanie domeny
- Wdrażanie bounded contexts i zarządzanie relacjami między kontekstami
- Relacje partnerstwo i współdzielone jądro
- Relacje customer–supplier i open host service
- Anti-corruption layer – ochrona przed korumpującymi wpływami
- Praktyczna implementacja Domain-Driven Design
- Faza analizy strategicznej i planowania
- Projektowanie klarownych granic bounded contexts
- Modelowanie warstwy domeny
- Orkiestracja warstwy aplikacji
- Implementacja warstwy infrastruktury
- Testowanie logiki domenowej w izolacji
- Command Query Responsibility Segregation (CQRS) – rozdzielenie modeli odczytu i zapisu
- Najczęstsze wyzwania implementacyjne i antywzorce
- Błędne rozumienie ubiquitous language i bounded contexts
- Przeinżynierowanie prostych domen
- Anemiczne modele domenowe i wzorce proceduralne
- Przerośnięte agregaty i niejasne granice spójności
- Niewystarczająca współpraca i pomijanie projektowania strategicznego
- Proliferacja usług domenowych wskazująca na złą organizację
- DDD w architekturze mikroserwisów
Metodyka zaproponowana przez Erica Evansa w książce „Domain-Driven Design: Tackling Complexity in the Heart of Software” (2003) stała się standardem dla organizacji budujących złożone systemy ze skomplikowaną logiką i zmiennymi wymaganiami. Istotą DDD jest ścisła współpraca ekspertów domenowych i programistów, tworzenie modelu wiernie odzwierciedlającego rzeczywistość biznesową oraz używanie wspólnego słownika – Ubiquitous Language.
Poprzez dzielenie systemu na logiczne Bounded Contexts z wyraźnymi granicami oraz tworzenie świadomych relacji między nimi, DDD wspiera architektury skalowalne, łatwe w utrzymaniu i zbieżne z celem biznesowym. Artykuł omawia podstawy DDD, wzorce strategiczne i taktyczne, praktyki wdrożeniowe oraz typowe pułapki w projektach komercyjnych.
Zrozumienie Domain-Driven Design – definicja, pochodzenie i główna filozofia
DDD wyrasta z obserwacji, że wiele zespołów przedkłada technikalia nad realia biznesowe, co skutkuje rozwiązaniami nieadekwatnymi do potrzeb. DDD przesuwa akcent z technologii na domenę – konkretny obszar wiedzy i aktywności biznesowej – i podporządkowuje jej model oraz decyzje projektowe.
Współczesne systemy rosną w złożoność: mają liczne reguły, wielu interesariuszy i szybko ewoluujące wymagania. DDD zapewnia strukturę, która utrzymuje zgodność architektury ze strategią i celami biznesowymi przez cały cykl życia produktu.
Warunkiem sukcesu jest zrozumienie domeny poprzez intensywną współpracę z ekspertami. Programiści stopniowo stają się współ-ekspertami, a decyzje techniczne odzwierciedlają fakty biznesowe, nie preferencje architektoniczne. Wspólny model i język eliminują rozjazdy mentalnych modeli między IT a biznesem.
Projektowanie strategiczne – ustanawianie struktury domeny poprzez bounded contexts i ubiquitous language
Projektowanie strategiczne działa „ponad” wzorcami taktycznymi: porządkuje duże systemy, definiuje granice odpowiedzialności i mechanizmy komunikacji. Dwa filary to Ubiquitous Language i Bounded Contexts.
Ubiquitous language – podstawa wspólnego zrozumienia
Ubiquitous Language to precyzyjny, współtworzony słownik pojęć zrozumiały dla ekspertów domenowych, programistów, architektów i analityków. Powinien być konsekwentnie używany w kodzie, dokumentacji, diagramach i rozmowach. Konsekwencja językowa minimalizuje nieporozumienia, upraszcza modelowanie i przyspiesza decyzje.
Eksperci kwestionują niejasne terminy; programiści wyrażają logikę słownictwem domeny. Dzięki temu kod czyta się jak logikę biznesową, a nie techniczne rusztowanie.
Bounded contexts – tworzenie klarownych granic organizacyjnych
Bounded Context to zakres, w którym model ma spójne znaczenie i nie ulega dwuznaczności. Ten sam termin („produkt”, „klient”) może znaczyć co innego w sprzedaży i w magazynie — i to jest w porządku, jeśli każdy kontekst utrzymuje własny model i język, a integracje są jawnie zarządzane.
Bounded Context żyje w Solution Space, a subdomena w Problem Space. Jeden kontekst może odwzorowywać jedną lub wiele subdomen – ważne, by wewnątrz zachować jednoznaczność i spójność. Dobre granice umożliwiają autonomię zespołów i skalowalność rozwoju.
Subdomeny – kategoryzowanie złożoności domeny
Aby właściwie inwestować wysiłek i zasoby, warto rozróżniać typy subdomen:
- Core Domain – krytyczny obszar przewagi konkurencyjnej; wymaga najlepszych inżynierów, najwyższej jakości projektowej i największej inwestycji (często to niewielki procent kodu dostarczający większość wartości);
- Supporting Subdomains – funkcje wspierające core domain, specyficzne dla organizacji; warto je szyć na miarę, choć nie zawsze wymagają najbardziej wyrafinowanej architektury;
- Generic Subdomains – zdolności generyczne (np. fakturowanie, uwierzytelnianie, płatności), które najlepiej pozyskiwać jako gotowe rozwiązania, oszczędzając zasoby na elementy różnicujące.
Taki podział porządkuje alokację zasobów, decyzje build vs. buy i dobór kompetencji zespołów.
Wzorce projektowania taktycznego – podstawowe elementy modeli domenowych
Wzorce taktyczne przekuwają rozumienie domeny w kod: pozwalają czysto wyrażać reguły i inwarianty oraz testować logikę w izolacji.
Encje i obiekty wartości – rozróżnianie obiektów domeny
Warstwa domeny operuje głównie na dwóch typach obiektów. Dla przejrzystości porównanie kluczowych własności wygląda następująco:
| Cecha | Entity (Encja) | Value Object (Obiekt wartości) |
|---|---|---|
| Tożsamość | Istotna i trwała w czasie (ID); | Brak – liczą się wyłącznie atrybuty; |
| Równość | Po tożsamości; | Po wszystkich atrybutach; |
| Mutowalność | Zwykle mutowalne (zmienia się stan); | Z zasady niemutowalne; |
| Przykłady | Klient, Zamówienie, Produkt; | Money, Address, Numer telefonu; |
| Implikacje projektowe | Trzeba strzec spójności i inwariantów; | Łatwe kopiowanie, porównywanie, współdzielenie. |
Wybór typu obiektu determinuje decyzje projektowe: obiekty wartości upraszczają rozumowanie i testy, encje wymagają zarządzania całym cyklem życia i regułami zmian.
Agregaty i korzenie agregatów – granice spójności
Aggregate to klaster encji i obiektów wartości będący jednostką spójności i transakcji. Inwarianty obowiązują wewnątrz agregatu i muszą być utrzymane po każdej modyfikacji.
Aggregate Root to jedyny publiczny punkt dostępu; wszystkie zmiany przechodzą przez korzeń, który egzekwuje reguły. To zabezpiecza model przed niespójnymi modyfikacjami.
Granice agregatu powinny być małe i skupione na konkretnych inwariantach. Gdy obiekty nie muszą zmieniać się razem – należą do osobnych agregatów i komunikują się przez identyfikatory lub zdarzenia.
Usługi domenowe – kapsułkowanie operacji, które nie pasują do obiektów
Nie każdą regułę sensownie osadzić w encji lub obiekcie wartości. Domain Service kapsułkuje bezstanową logikę przekrojową (np. kalkulację kosztu wysyłki), nie mieszając jej z odpowiedzialnościami agregatów. Usługi domenowe wyrażają czystą logikę biznesową; orkiestracja i infrastruktura należą do warstwy aplikacji.
Repozytoria – abstrakcja warstwy trwałości
Repository ukrywa mechanizmy persystencji i pozwala operować agregatami jak na kolekcjach w pamięci. Izoluje domenę od infrastruktury, ułatwia testowanie i daje elastyczność zmiany technologii.
W DDD repozytorium istnieje dla korzenia agregatu (nie dla każdej encji). Operacje koncentrują się na pobieraniu po tożsamości, zapisywaniu oraz zapytaniach według reguł (np. Specifications).
Fabryki i tworzenie obiektów wartości
Factory kapsułkuje złożoną inicjalizację agregatów i obiektów wartości: walidacje, inwarianty, koordynację wielu elementów. Zapewnia, że obiekty powstają w stanie ważnym i upraszcza warstwę aplikacji.
Zdarzenia domenowe – rejestrowanie zmian stanu i efektów ubocznych
Domain Events modelują znaczące zmiany w domenie (np. OrderPlaced, PaymentProcessed). Ujawniamy skutki i rozsprzęglamy agregaty – zamiast bezpośrednich wywołań, publikujemy zdarzenia i reagujemy na nie w odpowiednich handlerach.
Zdarzenia domenowe różnią się od integracyjnych: zwykle są przetwarzane synchronicznie w obrębie tego samego procesu, by zachować spójność lokalną.
Event storming i współdzielone modelowanie domeny
Event Storming (Alberto Brandolini) to szybka technika warsztatowa do odkrywania procesów, identyfikacji agregatów i wyznaczania granic kontekstów na wspólnym „płótnie”.
Proces event storming – od odkrywania do bounded contexts
Typowy przebieg warsztatu obejmuje następujące kroki:
- Identyfikacja Domain Events – zdarzeń wyrażanych w czasie przeszłym i układanych chronologicznie.
- Wyszukanie Commands – działań użytkowników lub systemów wyzwalających zdarzenia.
- Określenie Actors – ról inicjujących komendy (ludzie, systemy).
- Aggregate identification – grupowanie komend i zdarzeń w naturalne jednostki spójności.
- Wyznaczenie Bounded Context boundaries – łączenie agregatów, które zmieniają się razem; rozdzielanie tych, które ewoluują niezależnie.
Wdrażanie bounded contexts i zarządzanie relacjami między kontekstami
Wyraźne granice wspierają skalowalność i niezależność zespołów, ale wymagają świadomego modelowania relacji i kontraktów.
Relacje partnerstwo i współdzielone jądro
W Partnership dwa konteksty o współzależnych celach ściśle koordynują zmiany, dzieląc odpowiedzialność i ryzyko. Sprawdza się przy częstej interakcji, ale wymaga świetnej komunikacji.
Shared Kernel polega na współdzieleniu niewielkiej części modelu (np. obiektów wartości). Wprowadza sprzężenie i wymaga rygoru zmian, dlatego należy go używać oszczędnie.
Relacje customer–supplier i open host service
W relacji Customer–Supplier dostawca definiuje kontrakt, a klient od niego zależy; to asymetria często odpowiadająca strukturze organizacyjnej.
Open Host Service udostępnia ustandaryzowany interfejs, z którego może korzystać wiele klientów – zamiast tworzenia wielu relacji punkt–punkt.
Anti-corruption layer – ochrona przed korumpującymi wpływami
Anti-Corruption Layer (ACL) tłumaczy pojęcia i dane między niekompatybilnymi modelami (np. przy integracji z legacy), chroniąc lokalny model przed degradacją. ACL absorbuje złożoność i utrzymuje klarowność pojęć w nowym systemie.
Praktyczna implementacja Domain-Driven Design
Teoria jest fundamentem, lecz wdrożenie wymaga pragmatyzmu, iteracji i dyscypliny technicznej.
Faza analizy strategicznej i planowania
Warto zacząć od warsztatów z ekspertami i architektami, identyfikacji subdomen oraz wstępnych granic kontekstów (np. poprzez Event Storming). Wyraźnie wskaż, co jest core domain, co wspierające, a co generyczne – to ukierunkuje inwestycje i decyzje build vs. buy.
Projektowanie klarownych granic bounded contexts
Granice powstają iteracyjnie – wraz z lepszym zrozumieniem domeny. Każdy kontekst powinien mieć jasną odpowiedzialność i właściciela (zespół), co ułatwia spójne decyzje i rozliczalność jakości.
Modelowanie warstwy domeny
Logika biznesowa powinna być niezależna od frameworków i baz. Agregaty i obiekty wartości wyrażają operacje domenowe (np. AddLineItem, ConfirmPayment), egzekwując inwarianty przez kontrolowane interfejsy i fabryki.
Orkiestracja warstwy aplikacji
Warstwa aplikacji orkiestruje przypadki użycia: odbiera żądania, woła domenę i repozytoria, koordynuje persystencję oraz komunikację. Pozostaje cienka i wolna od reguł biznesowych.
Implementacja warstwy infrastruktury
Infrastruktura realizuje interfejsy zdefiniowane w domenie (persystencja, messaging, integracje), utrzymując separację od logiki biznesowej i umożliwiając wymienność technologii.
Testowanie logiki domenowej w izolacji
Testy jednostkowe modeli domenowych w pamięci są szybkie i wiarygodne; weryfikują inwarianty, sekwencje zmian stanu i reguły bez udziału infrastruktury.
Command Query Responsibility Segregation (CQRS) – rozdzielenie modeli odczytu i zapisu
CQRS rozdziela model zapisu (komendy) od modelu odczytu (zapytania). Model zapisu dba o reguły i spójność, a model odczytu jest zdenormalizowany i zoptymalizowany pod potrzeby interfejsu oraz wydajność. Pozwala to utrzymać prosty, poprawny model zapisu i szybkie, elastyczne odczyty.
Sprawdza się tam, gdzie wzorce zapisu i odczytu znacząco się różnią (np. złożone składanie zamówień vs. proste, częste sprawdzanie statusu).
Najczęstsze wyzwania implementacyjne i antywzorce
Błędne rozumienie ubiquitous language i bounded contexts
Ten sam termin z innym znaczeniem w różnych miejscach systemu to prosta droga do chaosu. Wymagaj precyzji definicji, dokumentuj uzgodnienia i pilnuj granic kontekstów.
Przeinżynierowanie prostych domen
DDD nie jest panaceum. W prostych aplikacjach CRUD rozbudowane agregaty czy zdarzenia bywają przerostem formy nad treścią. Stosuj DDD tam, gdzie złożoność naprawdę tego wymaga.
Anemiczne modele domenowe i wzorce proceduralne
Modele bez logiki łamią ducha DDD. Reguły powinny żyć w obiektach domenowych, a nie w usługach proceduralnych.
Przerośnięte agregaty i niejasne granice spójności
Zbyt szerokie agregaty powodują blokady, trudne testy i niejasną semantykę. Projektuj najmniejsze możliwe granice utrzymujące wymagane inwarianty.
Niewystarczająca współpraca i pomijanie projektowania strategicznego
Rozpoczynanie implementacji bez wspólnego odkrywania domeny zwykle kończy się reworkiem. Inwestuj w warsztaty i mapowanie kontekstów, zanim zejdziesz do taktyki.
Proliferacja usług domenowych wskazująca na złą organizację
Nadmierna liczba usług domenowych często oznacza, że logika powinna wrócić do encji lub zostać zawężona w agregatach.
DDD w architekturze mikroserwisów
DDD naturalnie współgra z mikroserwisami: jeden mikroserwis często odpowiada jednemu bounded context, utrzymując własny model, bazę i granice spójności; komunikacja odbywa się przez API lub zdarzenia asynchroniczne.
Nie ma jednak dogmatu 1:1. To domena powinna dyktować granice: czasem jeden kontekst wymaga kilku usług (powody wdrożeniowe, skalowanie), a czasem kilka kontekstów współistnieje w jednej usłudze. Decyzje architektoniczne muszą wypływać z modelu, nie z mody.
