Enforces full DDD conventions for TypeScript/Next.js: folder structure, layer boundaries, functional patterns, and ubiquitous language.
100
100%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
| Layer | May Import From | Never Imports From |
|---|---|---|
domain/ | shared/ only | Everything else |
application/ | domain/, shared/ | infrastructure/, frameworks |
infrastructure/ | application/, domain/, shared/ | — |
components/ | Module index.ts only | domain/, application/, infrastructure/ |
lib/[context].ts | application/ + infrastructure/ | — (composition root only) |
Immediately actionable steps: see Workflow · Pass/fail criteria: see Violations Checklist
src/
├── app/api/[context]/route.ts # HTTP adapter only
├── modules/
│ └── [context-name]/ # kebab-case, domain noun
│ ├── domain/
│ │ ├── types.ts # All domain types
│ │ ├── [aggregateName].ts # Pure functions on aggregate
│ │ ├── [valueObjectName].ts # Factory + guard functions
│ │ ├── events/[eventName].ts # Event type + factory
│ │ ├── repositories/[name]Repository.ts # Interface type only
│ │ └── services/[name].ts # Pure domain logic
│ ├── application/
│ │ ├── commands/[name].ts # Command type + handler
│ │ ├── queries/[name].ts # Query type + handler
│ │ └── ports/[name].ts # Infra interface types
│ ├── infrastructure/
│ │ ├── persistence/[name]Repository.ts
│ │ ├── persistence/[name]Mapper.ts
│ │ └── adapters/[name]Adapter.ts
│ └── index.ts # Only export what other contexts may use
├── components/
│ ├── [context]/ # Context-scoped components
│ │ └── [ComponentName].tsx # Accepts view model props only
│ └── ui/ # Shared primitive UI (no domain awareness)
├── shared/domain/
│ ├── Result.ts # Result<T, E> + ok/err/map/flatMap
│ ├── DomainEvent.ts # Base event type + createEvent
│ ├── identity.ts # Brand<T,B>, Id<B>, createId, castId
│ └── guards.ts
└── lib/[context].ts # Composition rootRules:
index.ts is the only file other modules may import from. Cross-module imports into domain/, application/, or infrastructure/ are forbidden.shared/ contains generic mechanics only. No business logic.api, data, utils.readonly; arrays are readonly T[]; functions return new values, never mutate.Result<T, E>; never throw for domain errors.Id<B> (branded string); never raw string as an identifier.any — banned in domain/ and application/ without exception.data, info, manager, processor, util, or unqualified service for domain concepts.GLOSSARY.md at repo root.Established once per project in shared/domain/. Copy verbatim; these files rarely change. Full implementations: docs/PRIMITIVES.md.
| File | Key exports |
|---|---|
Result.ts | Result<T, E>, ok, err, map, flatMap |
identity.ts | Brand<T, B>, Id<B>, createId, castId |
DomainEvent.ts | DomainEvent<T, P>, createEvent |
Full examples (value objects, aggregates, repository interfaces): docs/EXAMPLES.md.
// modules/ordering/domain/types.ts
export type OrderId = Id<'OrderId'>;
export type OrderStatus = 'draft' | 'placed' | 'fulfilled' | 'cancelled'; // string unions over enums
export type Order = {
readonly id: OrderId;
readonly customerId: CustomerId;
readonly status: OrderStatus;
readonly lines: readonly OrderLine[];
};Result<[UpdatedAggregate, ...DomainEvents]>. Aggregate functions never call repositories.save always takes events alongside the aggregate.// modules/ordering/application/commands/placeOrder.ts
export type PlaceOrderCommand = { orderId: string; customerId: string };
export type PlaceOrderHandler = (cmd: PlaceOrderCommand) => Promise<Result<void>>;
export const createPlaceOrderHandler =
(orders: OrderRepository): PlaceOrderHandler =>
async (cmd) => {
const order = await orders.findById(castId('OrderId')(cmd.orderId));
if (!order) return err('Order not found');
const result = placeOrder(order);
if (!result.ok) return result;
const [updated, event] = result.value;
await orders.save(updated, [event]);
return ok(undefined);
};Handlers are curried — dependencies first, command second. Map raw primitives to domain types at the entry point.
Full example: docs/EXAMPLES.md.
Rules: Implementations are factory functions. Mappers are the only place DB row types appear. DB/ORM/HTTP clients imported only here. Events go to outbox in the same transaction — never fire-and-forget. External API adapters translate to domain types before crossing the boundary.
Full example: docs/EXAMPLES.md.
No domain logic, no direct repository calls. Import only from the module's index.ts or application/.
// lib/ordering.ts
export const createOrderingHandlers = () => {
const orders = createOrderRepository();
const email = createSendGridEmailAdapter();
return {
placeOrder: createPlaceOrderHandler(orders, email),
cancelOrder: createCancelOrderHandler(orders),
};
};One file per context. Wiring only.
Context A needs data from context B:
index.tsComponents receive view models — plain objects shaped for the UI. No domain types, no branded IDs, no Result in props. Full example: docs/EXAMPLES.md.
Rules:
components/[context]/ for context-tied components; components/ui/ for primitives with no domain awareness.ordering, inventory); never api, data, utils.domain/, application/, infrastructure/, and an empty index.ts.types.ts with aggregate and value object shapes.Result<[UpdatedAggregate, ...DomainEvents]>.domain/repositories/.application/.lib/[context].ts.index.ts exports only what other contexts need.domain/ or application/index.tsany in domain/ or application/Resultsave called without passing eventsdomain/, application/, or infrastructure/skills
ddd-functional-nextjs