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.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

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