CtrlK
BlogDocsLog inGet started
Tessl Logo

workout/ddd-functional-nextjs

Enforces full DDD conventions for TypeScript/Next.js: folder structure, layer boundaries, functional patterns, and ubiquitous language.

100

Quality

100%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdskills/ddd-functional-nextjs/

name:
ddd-functional-nextjs
description:
Implements and enforces full DDD (domain-driven design) conventions for TypeScript/Next.js projects using functional patterns. Validates folder structure against DDD layers, enforces dependency rules between domain/application/infrastructure layers, generates domain module scaffolding, and reviews code for bounded context violations. Use when the user asks about domain-driven design, project architecture, bounded contexts, layer separation, clean architecture, hexagonal architecture, ports and adapters, or organising a TypeScript/Next.js codebase with a domain model, aggregate, value object, or ubiquitous language.

DDD Compliance — TypeScript / Next.js (Functional)

Quick Reference

LayerMay Import FromNever Imports From
domain/shared/ onlyEverything else
application/domain/, shared/infrastructure/, frameworks
infrastructure/application/, domain/, shared/
components/Module index.ts onlydomain/, application/, infrastructure/
lib/[context].tsapplication/ + infrastructure/— (composition root only)

Immediately actionable steps: see Workflow · Pass/fail criteria: see Violations Checklist


Folder Structure

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 root

Rules:

  • 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.
  • Context names must be domain nouns. Never api, data, utils.

Universal Rules

  • Immutability — all type fields are readonly; arrays are readonly T[]; functions return new values, never mutate.
  • Result over throw — factory and mutation functions always return Result<T, E>; never throw for domain errors.
  • Branded IDs — every aggregate identifier uses Id<B> (branded string); never raw string as an identifier.
  • No any — banned in domain/ and application/ without exception.
  • Domain language — all names (files, types, functions, variables, DB columns, API endpoints) use ubiquitous language; never data, info, manager, processor, util, or unqualified service for domain concepts.
  • One name per concept — consistent across code, API, DB, and docs; maintain GLOSSARY.md at repo root.

Shared Primitives

Established once per project in shared/domain/. Copy verbatim; these files rarely change. Full implementations: docs/PRIMITIVES.md.

FileKey exports
Result.tsResult<T, E>, ok, err, map, flatMap
identity.tsBrand<T, B>, Id<B>, createId, castId
DomainEvent.tsDomainEvent<T, P>, createEvent

Domain Layer

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[];
};
  • Mutation functions return Result<[UpdatedAggregate, ...DomainEvents]>. Aggregate functions never call repositories.
  • save always takes events alongside the aggregate.

Application Layer

// 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.


Infrastructure Layer

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.


API Routes

Full example: docs/EXAMPLES.md.

No domain logic, no direct repository calls. Import only from the module's index.ts or application/.


Composition Root

// 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.


Cross-Context Communication

Context A needs data from context B:

  • Read — call a query handler exported from B's index.ts
  • React — subscribe to B's domain events; maintain A's own representation

Components

Components 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:

  • All formatting, label mapping, and display logic lives in the query handler, not the component.
  • components/[context]/ for context-tied components; components/ui/ for primitives with no domain awareness.
  • Components never call handlers directly — data fetching happens in Next.js page/layout server components or route handlers, which pass view models down as props.

Workflow: Creating a New Bounded Context

  1. Name the context — domain noun (e.g. ordering, inventory); never api, data, utils.
  2. Scaffold — create domain/, application/, infrastructure/, and an empty index.ts.
  3. Define domain typestypes.ts with aggregate and value object shapes.
  4. Implement value objects — one file per concept.
  5. Implement aggregates — pure functions returning Result<[UpdatedAggregate, ...DomainEvents]>.
  6. Declare repository interfaces — in domain/repositories/.
  7. Write command and query handlers — in application/.
  8. Implement infrastructure — repositories, mappers, adapters; events to outbox in the same transaction.
  9. Wire the composition rootlib/[context].ts.
  10. Expose the public APIindex.ts exports only what other contexts need.
  11. Run the violations checklist — scan every file before marking complete.

Violations to Flag

  • Domain logic inside a route or command handler
  • Repository interface returning a DB row or primitive
  • DB/ORM import inside domain/ or application/
  • Value object as a raw primitive with no factory/validation
  • Cross-module import bypassing index.ts
  • any in domain/ or application/
  • Aggregate mutation not returning Result
  • Domain event emitted outside a domain function
  • In-place mutation of a domain type
  • save called without passing events
  • Component props containing domain types or branded IDs
  • Formatting or display logic inside a component instead of a query handler
  • Component importing from domain/, application/, or infrastructure/

skills

ddd-functional-nextjs

SKILL.md

tile.json