Domain-driven design z agentami AI
Domain-driven design (DDD) z agentami AI działa, gdy trzy rzeczy żyją w repozytorium, a nie w głowach ludzi: język wszechobecny w pliku instrukcji agenta, niezmienniki agregatu jako testy zatwierdzone przez człowieka przed implementacją oraz granice ograniczonych kontekstów jako reguła zależności, która zatrzymuje CI. Kod pisze agent; te trzy artefakty dowodzą, że nadal pasuje do domeny.
Pół roku w projekcie i „Order” oznacza trzy różne rzeczy: wiersz w tabeli orders, DTO w checkoucie i view model w Reakcie. Agent wymyśla pola, których prawdziwa encja nie ma, każda refaktoryzacja wciąga persystencję z powrotem do warstwy domeny, a moduł Shipping po cichu importuje OrderRepository. Nikt już nie czyta każdego diffa, więc nikt nie widzi, że model i język się rozjechały, dopóki nie pokaże tego zgłoszenie błędu.
Ta strona jest dla programistów, którzy modelują domenę z agentem, i dla tech leadów, którzy odpowiadają za granice między kontekstami. Należy do etapu planowania: specyfikacja mówi, co zbudować, a ta strona pilnuje, żeby agent zbudował to w pojęciach twojej domeny.
Co daje ci konfiguracja DDD dla agentów
Dział zatytułowany „Co daje ci konfiguracja DDD dla agentów”- Plik glosariusza, który każde narzędzie wczytuje samo, ograniczony do jednego kontekstu.
- Zestaw testów niezmienników jednego agregatu, który zatwierdzasz, zanim powstanie implementacja, oraz sprawdzenie z podłożonym błędem, które dowodzi, że te testy potrafią się wywrócić.
- Regułę
dependency-cruiser, która zamienia „Shipping nie sięga do wnętrza Ordering” w czerwony build w CI. - Pięć gotowych promptów: wydobycie języka, testy niezmienników, implementacja agregatu, szkic reguły granic i audyt dryfu glosariusza.
W jakiej kolejności powstają artefakty DDD?
Dział zatytułowany „W jakiej kolejności powstają artefakty DDD?”Kolejność waży więcej niż jakikolwiek pojedynczy prompt. Każdy artefakt jest sprawdzianem dla następnego kroku, więc agent, który przeskoczy krok, wyprodukuje kod, którego nie zweryfikujesz bez czytania.
| Krok | Artefakt | Kto zatwierdza | Co sprawdza |
|---|---|---|---|
| 1 | Glosariusz dla każdego kontekstu (CLAUDE.md / AGENTS.md / reguła projektu w Cursorze) | Ekspert domenowy + tech lead | Nazwy w kodzie, promptach i testach |
| 2 | Testy niezmienników każdego agregatu | Programista, który jest właścicielem kontekstu | Zachowanie agregatu |
| 3 | Implementacja agregatu | Nikt linijka po linijce: testy z kroku 2 | — |
| 4 | Reguła granic w CI | Tech lead | Importy między kontekstami |
Kroki 1 i 4 to decyzje architektoniczne. Zapisz, dlaczego granica istnieje, w rekordzie decyzji architektonicznej, żeby agent mógł przeczytać powód, a nie tylko regułę.
Krok 1: Umieść język wszechobecny tam, gdzie agent go czyta
Dział zatytułowany „Krok 1: Umieść język wszechobecny tam, gdzie agent go czyta”Język wszechobecny to słownictwo wspólne dla programistów i ekspertów domenowych. Umieść go w pliku instrukcji kontekstu, do którego należy, a nie w jednym olbrzymim pliku w katalogu głównym. Dzięki temu agent pracujący w Shipping nigdy nie zobaczy definicji „Account” z Billing. Każdy glosariusz niech mieści się na jednym ekranie; przycinanie plików kontekstu wyjaśnia, dlaczego długie instrukcje są ignorowane.
Treść jest w każdym narzędziu taka sama. Różni się plik i sposób wczytywania:
Utwórz regułę projektu w .cursor/rules/ dla kontekstu Ordering i zacommituj ją. Przewodnik po regułach projektu w Cursorze pokazuje, jak ograniczyć regułę do ścieżek kontekstu, żeby glosariusz działał tylko wtedy, gdy agent pracuje w src/contexts/ordering/.
Wpisz glosariusz do src/contexts/ordering/CLAUDE.md. Claude Code wczytuje pliki CLAUDE.md z katalogów powyżej katalogu roboczego przy starcie, a plik z podkatalogu dopiero wtedy, gdy czyta pliki w tym podkatalogu. Glosariusz Ordering trafia więc do kontekstu dokładnie wtedy, gdy agent dotyka kodu Ordering. Od v2.1.277 (kanał latest) Claude Code czyta AGENTS.md w projekcie bez CLAUDE.md; jeśli repozytorium ma główny CLAUDE.md, nazwij glosariusz każdego kontekstu również CLAUDE.md.
Wpisz glosariusz do src/contexts/ordering/AGENTS.md. Codex 0.157.1 łączy wszystkie pliki AGENTS.md od katalogu głównego projektu w dół, ale tylko do bieżącego katalogu roboczego. Zagnieżdżony plik wczyta się więc wtedy, gdy uruchomisz Codeksa w tym katalogu: codex -C src/contexts/ordering albo codex exec -C src/contexts/ordering "…". Od 0.150.0 niezaufany projekt nie dostarcza swojego AGENTS.md. Świeży checkout w CI może być niezaufany, dlatego audyt dryfu opisany niżej wskazuje pliki glosariusza wprost w prompcie i nie zależy od automatycznego wczytania AGENTS.md; o zaufaniu do projektu przeczytasz na stronie o uprawnieniach i piaskownicach.
Glosariusz wystarczająco precyzyjny dla agenta podaje rolę DDD każdego pojęcia i zapisuje niezmienniki jako reguły, a nie prozę:
# Ordering context — ubiquitous language
- **Order**: a customer's request to buy products. Aggregate root.- **LineItem**: one product and its quantity inside an Order. Value object, immutable.- **OrderStatus**: Pending | Paid | Shipped | Delivered | Cancelled.- **Customer**: referenced by `CustomerId` only; owned by the Customers context.
Invariants:- An Order has at most 10 distinct LineItems.- Status moves only forward: Pending → Paid → Shipped → Delivered. Cancelled is reachable from Pending and Paid only.- `total` is derived from LineItems and never set directly.
Rules for agents:- No persistence code (ORM decorators, `save()`) in `domain/`.- Other contexts use `index.ts` and `events.ts` only.- A term not in this list does not exist. Ask before inventing one.Jeśli język istnieje tylko w notatkach ze spotkań i starych ticketach, niech agent przygotuje szkic, a ekspert domenowy go poprawi. Recenzujesz szkic, a nie kod, który będzie go używał.
Krok 2: Najpierw zatwierdź niezmienniki agregatu jako testy
Dział zatytułowany „Krok 2: Najpierw zatwierdź niezmienniki agregatu jako testy”Agregat istnieje po to, by chronić swoje niezmienniki, więc niezmienniki są kryteriami akceptacji. Poproś o testy przed implementacją i zatwierdź je sam: to jest kontrakt, który recenzujesz zamiast diffa. To samo podejście stosują wykonywalne kryteria akceptacji dla historyjek użytkownika.
Punkt 3 celowo żąda wszystkich 25 par. Małą maszynę stanów taniej przetestować wyczerpująco niż losowo. Losowe sekwencje poleceń tylko od czasu do czasu docierają do późnego stanu, takiego jak Shipped, więc test właściwości może przy domyślnej liczbie przebiegów przeoczyć błędne przejście z tego stanu; tabela 25 wierszy sprawdza każdą parę przy każdym uruchomieniu. Testy oparte na właściwościach stosuj do niezmienników na dużych przestrzeniach wejść, takich jak sumy i ilości, a tabele do przejść stanów.
Test tabelaryczny jest na tyle krótki, że przeczytasz go w całości:
const STATUSES: OrderStatus[] = ['Pending', 'Paid', 'Shipped', 'Delivered', 'Cancelled'];
const ALLOWED: Record<OrderStatus, OrderStatus[]> = { Pending: ['Paid', 'Cancelled'], Paid: ['Shipped', 'Cancelled'], Shipped: ['Delivered'], Delivered: [], Cancelled: [],};
for (const from of STATUSES) { for (const to of STATUSES) { const allowed = ALLOWED[from].includes(to); it(`${from} → ${to} is ${allowed ? 'allowed' : 'rejected'}`, () => { const order = orderIn(from); // helper testowy: buduje Order w danym statusie if (allowed) { order.changeStatus(to); expect(order.status).toBe(to); } else expect(() => order.changeStatus(to)).toThrow(OrderInvariantError); }); }}Gdy zatwierdzisz specyfikację, nie pozwól agentowi implementującemu jej zmieniać: ochrona wyroczni pokazuje reguły uprawnień i kontrolę w CI dla każdego narzędzia.
Krok 3: Niech agent zaimplementuje agregat pod testy
Dział zatytułowany „Krok 3: Niech agent zaimplementuje agregat pod testy”Skoro testy są ustalone, prompt implementacyjny może być krótki. Musi dodać to, czego testy nie widzą, czyli kształt DDD: zachowanie na agregacie, brak setterów i brak persystencji.
Ostatnia linijka tego promptu to twój skrót do recenzji. Porównujesz krótką listę publicznych składowych z glosariuszem zamiast czytać klasę. Składowa bez pokrycia w glosariuszu, na przykład applyDiscount(), to zmyślone zachowanie: usuń ją albo świadomie dopisz pojęcie do glosariusza.
Potem udowodnij, że testy potrafią się wywrócić. Podłóż ręcznie jeden błąd, na przykład zmień >= 10 na >= 11 w sprawdzeniu liczby pozycji, i uruchom specyfikację. Jeśli zostaje zielona, jest słabsza, niż wygląda; siła wyroczni opisuje testy mutacyjne, które robią to na dużą skalę.
Krok 4: Pilnuj ograniczonych kontekstów regułą zależności w CI
Dział zatytułowany „Krok 4: Pilnuj ograniczonych kontekstów regułą zależności w CI”Prompt, który każe agentowi „sprawdzić naruszenia granic”, to recenzja, a nie bramka: jego kod wyjścia nie zależy od tego, co znajdzie. Zamień granicę w deterministyczną bramkę za pomocą dependency-cruiser, a agenta użyj do pisania i utrzymywania reguły. Zainstaluj go z obsługiwaną wersją typescript (zobacz ostrzeżenie niżej):
npm i -D dependency-cruiser typescript@^6module.exports = { forbidden: [ { name: 'shipping-uses-ordering-public-api-only', severity: 'error', comment: 'Shipping reacts to Ordering events; see docs/adr/0007-context-map.md', from: { path: '^src/contexts/shipping/' }, to: { path: '^src/contexts/ordering/', pathNot: '^src/contexts/ordering/(index|events)\\.ts$', }, }, ], options: { tsPreCompilationDeps: true },};Uruchamiaj ją w CI obok testów. Kończy się niezerowym kodem wyjścia przy każdym naruszeniu o poziomie error:
npx depcruise src --config .dependency-cruiser.cjsIstniejące naruszenia to decyzja tech leada, a nie agenta. Albo najpierw je napraw, albo zacznij regułę od severity: 'warn', naprawiaj po jednym naruszeniu na pull request i przełącz na error, gdy lista będzie pusta. Strona o funkcjach dopasowania omawia reguły warstw, budżety złożoności i tę samą bramkę w innych stosach.
Jak każde narzędzie audytuje dryf glosariusza?
Dział zatytułowany „Jak każde narzędzie audytuje dryf glosariusza?”Reguła zależności łapie importy. Nie złapie nowej klasy Purchase, która dubluje Order, ani LineItem zmienionego po utworzeniu. To wyłapie przebieg agenta w trybie tylko do odczytu: zgłasza znaleziska, a decyduje człowiek. Każdy wariant zapisuje diff do .agents/tmp/pr.diff wewnątrz repozytorium, więc agent tylko do odczytu otworzy go bez dodatkowych uprawnień; dopisz .agents/tmp/ do .gitignore. Przepływ różni się między narzędziami:
Przełącz się na gałąź pull requesta, uruchom w terminalu mkdir -p .agents/tmp && git diff origin/main...HEAD > .agents/tmp/pr.diff i wklej agentowi prompt poniżej. Jeśli chcesz recenzji przy każdym pull requeście, porównaj opcje w botach do recenzji kodu, wśród nich Bugbot od Cursora.
Uruchom go nieinteraktywnie (w trybie headless) i tylko do odczytu. --permission-mode dontAsk odrzuca każde wywołanie narzędzia spoza listy dozwolonych:
mkdir -p .agents/tmpgit diff origin/main...HEAD > .agents/tmp/pr.diffclaude -p "$(cat .agents/prompts/glossary-drift.md)" \ --permission-mode dontAsk \ --allowedTools "Read,Grep,Glob" \ --output-format json > drift.jsonUruchom go z katalogu głównego repozytorium w piaskownicy tylko do odczytu. Prompt wymienia pliki glosariusza z nazwy, więc wczytają się, choć leżą poniżej katalogu roboczego:
mkdir -p .agents/tmpgit diff origin/main...HEAD > .agents/tmp/pr.diffcodex exec --sandbox read-only \ -o drift.md "$(cat .agents/prompts/glossary-drift.md)"Trzymaj prompt w .agents/prompts/glossary-drift.md, żeby każde narzędzie i każde zadanie CI uruchamiało ten sam tekst. Diff celowo leży wewnątrz repozytorium: w trybie dontAsk Claude Code bez pytania odmawia odczytu spoza katalogu roboczego, a audyt zgłosiłby wtedy „No drift found”, choć diffu w ogóle nie widział. W CI uruchamiaj go tylko na zaufanych gałęziach; dla pull requestów z forków postępuj według strony o uprawnieniach i piaskownicach, żeby ani instrukcje projektu, ani sekrety nie trafiły do niezaufanego kodu.
Skąd wiesz, że model nadal pasuje do domeny?
Dział zatytułowany „Skąd wiesz, że model nadal pasuje do domeny?”Wynik weryfikujesz przez artefakty, a nie przez czytanie agregatu:
| Kontrola | Co łapie | Kiedy działa | Kto zatwierdza |
|---|---|---|---|
| Specyfikacja niezmienników (krok 2) | Złamane reguły biznesowe | Każdy commit | Właściciel kontekstu zatwierdza ją raz |
| Przebieg z podłożonym błędem | Specyfikację, która nie potrafi się wywrócić | Przy zmianie specyfikacji | Właściciel kontekstu |
| Lista publicznych składowych kontra glosariusz (krok 3) | Zmyślone zachowania | Każde zadanie implementacyjne | Programista, w kilka minut |
Reguła depcruise (krok 4) | Importy między kontekstami | Każdy pull request, blokująco | Tech lead jest właścicielem reguły |
| Audyt dryfu glosariusza | Nowe synonimy, zmieniane obiekty wartości, przeciekającą persystencję | Każdy pull request, doradczo | Recenzent segreguje znaleziska |
Pull request jest gotowy, gdy pierwsze cztery kontrole są zielone, a każde znalezisko z audytu dryfu jest naprawione albo zamknięte zmianą glosariusza w tym samym pull requeście.
Co się psuje, gdy agenci robią DDD?
Dział zatytułowany „Co się psuje, gdy agenci robią DDD?”- Glosariusz rozjeżdża się z biznesem. Pojęcie zmienia się na spotkaniu, a agent trzyma się starego. Wyjście: zmieniaj glosariusz w tym samym pull requeście co nazwę w kodzie i uruchom na nim audyt dryfu z punktem 5.
- Jeden prompt obejmuje kilka kontekstów. Poprosisz o „model Order” bez nazwania kontekstu, a agent zleje Ordering, Shipping i Billing w jedną klasę. Wyjście: zaczynaj sesję w katalogu kontekstu (w Codeksie
-Calbo otwórz tylko ten folder) i nazwij kontekst w pierwszej linijce promptu. - Anemiczny model przechodzi testy. Agent wkłada reguły do
OrderService, aOrderzostawia jako gettery i settery; testy niezmienników przechodzą, bo wołają serwis. Wyjście: niech specyfikacja woła wyłącznie metody agregatu, i dodaj regułę dependency-cruiser, która zabrania specyfikacji agregatu importować serwisy, na przykładfrom: { path: '^src/contexts/[^/]+/domain/.*\\.test\\.ts$' }, to: { path: '/services/' }. Reguła zabraniającadomain/importować zservices/zostaje tu zielona, bo to serwis importuje domenę, a nie odwrotnie. Drugą połowę łapie punkt 2 audytu dryfu: publiczne gettery i settery, których nie uzasadnia żadne zachowanie z glosariusza. - Agent osłabia czerwony test niezmiennika. Zamiast naprawić agregat, luzuje asercję. Wyjście: zablokuj edycję
*.test.tsw sesji implementującej i dodaj kontrolę w CI z ochrony wyroczni. - Bramka granic jest zielona, bo niczego nie sprawdziła. Aktualizacja do
typescript7, zła ścieżkafrom.pathalbo przeniesiony folder sprawiają, że dependency-cruiser przegląda 0 modułów albo nic nie dopasowuje. Wyjście: dodaj kanarkowy krok CI, który wpisuje znany zły import dosrc/contexts/shipping/, uruchamiadepcruisei oznacza zadanie jako nieudane, jeśli ten przebieg zakończy się kodem 0. - Istniejący kod narusza nową regułę od pierwszego dnia. Włączenie bramki od razu na
errorblokuje każdy pull request. Wyjście: zacznij odwarn, naprawiaj po jednym naruszeniu na pull request i przełącz naerror, gdy lista będzie pusta.
Dokąd dalej z domain-driven design
Dział zatytułowany „Dokąd dalej z domain-driven design”- Behavior-driven development zamienia język wszechobecny w scenariusze Given/When/Then.
- Funkcje dopasowania architektury rozszerzają bramkę granic o warstwy, złożoność i budżety duplikacji.
- Mikroserwisy z agentami omawiają ograniczone konteksty, które są osobnymi usługami z własnymi kontraktami.
- Modernizacja legacy stosuje to samo podejście „najpierw glosariusz”, by wykroić konteksty z monolitu.