Decide and review the tactical design of code through the lenses from Learning
Domain-Driven Design (Vlad Khononov, O'Reilly), Parts II–III. This is not a generic
layering/cohesion check (use model-lens) and not a bug hunt (use code-review). It
asks two questions:
- Is this the right business-logic pattern for this subdomain? (decision tree)
- If it's a domain model, do the aggregates and value objects actually hold their invariants and publish events safely? (correctness rules)
Apply the relevant lenses, report findings grouped by lens, cite file:line, name the
rule, propose the fix. Skip lenses that don't apply rather than padding. If it's clean,
say so plainly.
First: classify the subdomain
Pattern choice follows from subdomain type. Before recommending, place the work:
- Core — competitive advantage, complex & volatile rules (pricing, netValue, compliance logic, ticket/booking lifecycle, freight-lane optimization). Deserves the expensive patterns.
- Supporting — bespoke but simple, mostly CRUD/validation around core. Keep it cheap.
- Generic — solved problems you'd buy/integrate (auth, PDF rendering, ERP connectors). Don't hand-roll a domain model here.
If the language is mostly CRUD ("create/update/list X"), it's simple. If it's processes, rules, and invariants ("escalate", "settle", "reconcile", "void", "authorize", "allocate"), it's complex.
Repo mapping (greenspark-aws)
Repo-specific anchors for the lenses (skip if applying this skill elsewhere):
- Generic subdomains here: auth via Amplify, PDF rendering, ERP connectors — integrate, don't model.
- Pattern placement: most
core-apiroutes/services are transaction-script-shaped orchestration. Real domain models belong inbackend/commonentities/domain andgslogic. Don't push a domain model into a Fastify handler; don't leave money/audit logic as a bare transaction script. - Event-driven backbone: greenspark-aws is event-driven — EventBridge with
eda-worker/erp-sync-workflow— so reliable event publishing (Lens 5) is critical. Sagas and process managers map onto that EventBridge + worker setup.
Lens 1 — Business-logic pattern decision tree (Ch. 10)
Walk the tree top-down; stop at the first match. The book's bias — and this repo's — is use the simplest pattern that works, escalate only when forced.
- Tracks money / needs a consistent audit log / needs deep behavioral analytics? → Event-sourced domain model (state is a stream of events; requires CQRS to query).
- Else — business logic genuinely complex (rules, invariants, algorithms)? → Domain model (aggregates + value objects; ports & adapters; testing pyramid).
- Else — complex data structures but logic is procedural validation? → Active record (objects encapsulate DB mapping; layered + service layer; testing diamond).
- Else → Transaction script (a procedure per operation; minimal layers; lean on end-to-end tests).
Validation check: if you called something a core subdomain but the best-fit pattern is transaction script/active record — or a supporting subdomain wants a full domain model — your subdomain classification is probably wrong. Flag the mismatch; it's a signal, not noise.
In this repo: see "Repo mapping" above for where each pattern lives (core-api vs
backend/common / gslogic).
Lens 2 — Architecture & testing follow the pattern (Ch. 8, 10)
Once the business-logic pattern is chosen, these are near-mechanical:
- Event-sourced → CQRS required (otherwise you can only fetch one instance by id).
- Domain model → ports & adapters (so aggregates/value objects stay persistence-ignorant). → testing pyramid (aggregates make perfect units).
- Active record → layered + application/service layer (logic that drives the active records). → testing diamond (logic spans service + record layers, so integration).
- Transaction script → minimal 3-layer. → reversed pyramid (lean on end-to-end).
- CQRS exception: worth it for any pattern when the same data needs multiple persistent read models — not only for event sourcing.
Flag combinations that fight each other: a domain model wired through a layered architecture that leaks persistence into the aggregate; an event-sourced model with no read-side projection.
Lenses 3–5 — Domain-model correctness (only when it's a domain model)
These three apply only on the domain-model branch of Lens 1. Each has full hard
rules in references/decision-trees.md — read that file before reviewing the matching
code, and review against every rule there, not just the first that catches.
- Lens 3 — Aggregate correctness (Ch. 6). Transaction boundaries, command-only state changes, thin app layer, concurrency guard, smallest boundary. Review each aggregate against ALL the hard rules in the reference — passing one rule does not make it clean.
- Lens 4 — Value objects & ubiquitous language (Ch. 6). Primitive obsession,
immutability, behavior-on-the-value, business-language names. See the reference for the
rules;
ddd-strategicowns the full ubiquitous-language lens. - Lens 5 — Reliable domain-event publishing (Ch. 9). Don't publish from inside the aggregate or before commit; use an outbox; route cross-aggregate flows through a saga or process manager. EDA-critical here. Outbox mechanics and saga-vs-process-manager are in the reference.
Lens 6 — Has the pattern been outgrown? (Ch. 11)
The signal is pain: adding a rule keeps getting harder, inconsistencies/duplication creep in. Recommend the next step up only when the pain is real:
- Transaction script → active record: data handling got gnarly; encapsulate the structures.
- Active record → domain model: rules/invariants multiplying, inconsistencies appearing. Start by extracting value objects, then make setters private — the compile errors show you where the real transaction boundaries are.
- Domain model → event-sourced: the business now needs the full history / audit / time-travel, not just current state.
Don't over-escalate: a stable, simple supporting subdomain does not need a domain model just because it's "important." Migration also runs downward — if a subdomain was demoted from core to supporting, recommend simplifying the domain model back down.
Output format
## DDD Tactical review
### Subdomain & pattern
- <feature> looks like a <core/supporting/generic> subdomain → recommend <pattern>
(because <decision-tree reason>). [mismatch note if classification seems off]
### Aggregate correctness
- [file:line] <rule broken> — <fix>
### Value objects / language
- [file:line] <primitive obsession / mutable VO> — model as <value object>
### Event publishing
- [file:line] <publishes before commit / no outbox> — <fix>
### Pattern fit over time
- <outgrown? recommend next step, or "appropriate, hold">
### Verdict
<one line: right pattern + the top 1–2 corrections, or "clean">
Lead with the highest-leverage finding. If a lens is clean, write "clean" — don't invent
findings to fill it. Read references/decision-trees.md when the pattern classification
is borderline (you need the verbatim decision tree), when reviewing aggregate/event code
in depth (optimistic-lock shape, command-style choice, outbox mechanics, saga vs process
manager), or when citing the book chapter/figure provenance behind a lens.