Domain-Driven Design (DDD) Skill
This skill provides guidelines for organizing code following Domain-Driven Design principles.
Core Principles
- Group by Domain, Not by Type - Organize files by business domain rather than technical type
- Clear Boundaries - Each domain has well-defined responsibilities
- Self-Documenting Structure - Folder names clearly communicate what the code does
- Colocation - Related code (components, utils, tests) lives together
Domain Organization Rules
✅ DO
- Create domain folders that match business concepts
- Keep domain-specific utilities inside domain folders
- Place tests in
__tests__/subfolders within each domain - Use clear, descriptive folder names
❌ DON'T
- Create generic folders like
src/helpers/,src/services/, orsrc/utils/directly undersrc/(a scopedsrc/lib/utils/for minimal cross-domain primitives is allowed) - Mix different domain concerns in the same folder
- Create folder hierarchies deeper than 3 levels below
src/(e.g.,src/components/auth/login-form/is allowed;src/components/auth/login-form/subform/is not) - Use abbreviations in folder names
Project Structure
Components Domain
src/components/
├── auth/ # Authentication components
│ ├── login-form/
│ ├── register-form/
│ └── password-reset/
├── dashboard/ # Dashboard components
│ ├── stats-card/
│ ├── activity-feed/
│ └── charts/
├── checkout/ # Checkout flow components
│ ├── cart-summary/
│ ├── payment-form/
│ └── order-confirmation/
├── forms/ # Reusable form components
│ ├── contact-form/
│ └── newsletter-form/
├── layout/ # Layout components
│ ├── header/
│ ├── footer/
│ └── sidebar/
├── shared/ # Cross-domain reusable components
│ ├── loading-spinner/
│ ├── error-boundary/
│ └── empty-state/
└── ui/ # Primitive UI components
├── button/
├── input/
└── modal/
Library Domain
src/lib/
├── api/ # API client utilities
│ ├── client.ts
│ ├── endpoints.ts
│ └── types.ts
├── auth/ # Authentication utilities
│ ├── session.ts
│ ├── tokens.ts
│ └── permissions.ts
├── email/ # Email automation
│ ├── templates.ts
│ ├── sender.ts
│ └── types.ts
├── payment/ # Payment processing
│ ├── stripe.ts
│ ├── checkout.ts
│ └── types.ts
└── utils/ # Generic utilities (keep minimal)
├── formatting.ts
└── validation.ts
Actions Domain (Next.js Server Actions)
src/actions/
├── auth/ # Auth-related actions
│ ├── index.ts # Barrel export
│ ├── login.ts
│ ├── logout.ts
│ └── register.ts
├── checkout/ # Checkout actions
│ ├── index.ts # Barrel export
│ ├── create-order.ts
│ ├── process-payment.ts
│ └── add-to-cart.ts
├── contact/ # Contact form actions
│ ├── index.ts
│ └── submit-form.ts
└── user/ # User management actions
├── index.ts
├── update-profile.ts
└── change-password.ts
Data Access Layer (DAL)
src/data/ # Data Access Layer - server-only
├── index.ts # Barrel export
├── user.ts # User data operations
├── product.ts # Product data operations
├── order.ts # Order data operations
└── auth.ts # Auth/session utilities
CRITICAL: All files in
src/data/must start withimport "server-only"to prevent accidental client-side imports. Seeserver-actions.instructions.mdfor security details.
Barrel Export Pattern
Use barrel exports (index.ts) for folders with multiple internal files.
Export Strategy by File Type
| File Type | Export Type | Reason |
|---|---|---|
| Components | export default |
Tree-shaking optimization (Next.js recommendation) |
| Helpers/Utils | export { name } |
Multiple functions per file |
| Types | export type { } |
Type-only exports |
| Actions | export { name } |
Named functions for clarity |
| Hooks | export { name } |
Named functions for clarity |
When to Create Barrel Exports
- ✅ Domain folders with 3+ files
- ✅ Component folders with multiple related components
- ✅ When you want to hide internal file structure
- ❌ Single-file folders (unnecessary)
Component Barrel Export
// src/components/auth/index.ts
// Components use "export default" in their files,
// barrel re-exports with names for convenient imports
export { default as LoginForm } from "./login-form";
export { default as RegisterForm } from "./register-form";
export { default as PasswordReset } from "./password-reset";
// Usage - Clean named imports
import { LoginForm, RegisterForm } from "@/components/auth";
Library Barrel Export
// src/lib/payment/index.ts
// Libraries use named exports throughout
export { createCheckout, validateCart } from "./checkout";
export { processPayment, refundPayment } from "./stripe";
export type { PaymentIntent, CheckoutSession } from "./types";
// Usage
import {
createCheckout,
processPayment,
type PaymentIntent,
} from "@/lib/payment";
Actions Barrel Export
// src/actions/checkout/index.ts
export { createOrder } from "./create-order";
export { processPayment } from "./process-payment";
export { addToCart } from "./add-to-cart";
// Usage
import { createOrder, addToCart } from "@/actions/checkout";
Import Rules
// ✅ CORRECT: Import from domain barrel
import { LoginForm } from "@/components/auth";
import { createCheckout } from "@/lib/payment";
import { createOrder } from "@/actions/checkout";
// ❌ INCORRECT: Deep imports when barrel exists
import LoginForm from "@/components/auth/login-form";
import { createCheckout } from "@/lib/payment/checkout";
Domain Boundaries
Identifying Domains
Ask these questions to identify domains:
- What business concept does this code represent?
- Who is the primary user of this functionality?
- What would change together?
Example Domain Identification
| Feature | Domain | Reason |
|---|---|---|
| Login form | auth |
Authentication concern |
| Product card | products or catalog |
Product display concern |
| Shopping cart | checkout |
Purchase flow concern |
| User settings | user or settings |
User management concern |
Avoiding Common Mistakes
❌ Generic Folder Anti-Patterns
# ❌ BAD: Generic folders at src/ root
src/
├── components/
├── helpers/ # What kind of helpers?
├── services/ # Too vague
├── utils/ # Catch-all folder
└── types/ # Types should live with their domain
Shared TypeScript Types
- If a type is used by 3 or more domains, place it in
src/lib/types/— the shared-types exception lives under the allowedsrc/lib/scope, not a genericsrc/types/root (which the anti-pattern above forbids). Export it from that folder's own barrel (src/lib/types/index.ts) and import it as@/lib/types. A centrally shared primitive has no owning domain, so do not route it through a domain barrel — that would invert the intended dependency direction (domains → shared, never shared → a domain). - Otherwise, colocate the type with its primary domain (e.g.,
src/lib/payment/types.ts,src/components/auth/types.ts) and re-export it through that domain's barrel when other domains need it.
✅ Domain-Oriented Structure
# ✅ GOOD: Domain-oriented
src/
├── components/
│ ├── auth/
│ ├── checkout/
│ └── shared/
├── lib/
│ ├── auth/
│ ├── payment/
│ └── api/
└── actions/
├── auth/
└── checkout/
Framework-Aware Logic Doesn't Belong in lib/
A function that calls a routing/navigation primitive from the framework (e.g. Next.js notFound(),
redirect()) is response-shaping, route-layer logic — not data-layer logic — even if it wraps a
data-fetching call. Colocate it with the route(s) that call it, not inside a generic lib/ domain
folder. Two independent, concrete reasons, not just a style preference:
lib/importing a routing primitive is itself a signal something's in the wrong layer —lib/should be framework-agnostic-ish data logic, testable without a router in scope.- A resolver in
lib/invites a second route to import and reuse it under a different not-found/error contract than the first route needed, silently coupling two routes' error handling together. Keeping it colocated per-route keeps that coupling from happening by construction.
❌ src/lib/orders/resolve-order.ts — calls notFound(), lives in a generic domain folder
✅ src/app/orders/[id]/resolve-order.ts — colocated with the one route that calls it
If the same resolver logic is genuinely needed by more than one route that don't share a parent
segment, that's a real signal worth surfacing rather than quietly duplicating or importing across
route boundaries — a shared, framework-private location (e.g. a Next.js underscore-prefixed folder
like app/_shared/, which is excluded from routing) is a reasonable resolution once more than one
unrelated route needs the exact same not-found/error contract.
Migration Strategy
When refactoring to DDD:
- Identify current pain points - What's hard to find?
- Map business domains - List all business concepts
- Plan folder structure - Design new organization
- Migrate incrementally - Move one domain at a time
- Update imports - Use find-and-replace for import paths
- Add barrel exports - Create index.ts files as you go
Checklist
Before creating new code, verify:
- Code belongs to an identifiable business domain
- Domain folder already exists or should be created
- Related tests will live in
__tests__/within the domain - Imports use domain paths (not deep internal paths)
- No generic "utils" or "helpers" at root level