| name | domain-driven-design |
| description | Guide for implementing domain entities, services, and ports following the project's DDD conventions. |
| metadata | {"short-description":"Project-specific DDD implementation guide"} |
Domain-Driven Design Skill
Guide for implementing domain entities, services, and ports following the project's DDD conventions.
When to use
- Creating a new domain entity
- Adding domain validation logic
- Implementing a new application service
- Adding ports and adapters
Domain Record Pattern
Domain records extend their DB row shape:
import type { TeamMediaAssetRowShape } from '@tx-agent-kit/db'
export interface MediaAssetRecord extends Omit<TeamMediaAssetRowShape, 'assetTypeData'> {
readonly assetTypeData: AssetTypeData | null
}
export interface MediaAssetRecord {
id: string
teamId: string
}
Domain Functions
- Use
Either for validation with typed errors
- Use
Option for computed values that might not exist
- Use plain returns for boolean guards and string transforms
- Never
throw — failures are values
New Entity Checklist
- Add table to
packages/infra/db/src/schema.ts
- Add migration SQL
- Add effect-schema for runtime row validation
- Add factory for tests
- Add domain record that extends row shape in
packages/core/src/domains/<domain>/domain/
- Add ports in
packages/core/src/domains/<domain>/ports/
- Add application service in
packages/core/src/domains/<domain>/application/
- Add adapter in
packages/core/src/domains/<domain>/adapters/
- Add API mapper in
apps/api/src/mappers/
- Add route handlers in
apps/api/src/routes/
- Add contract schemas in
packages/contracts/src/
- Wire into
apps/api/src/server-lib.ts
- Export from
packages/core/src/index.ts
- Run
pnpm type-check && pnpm lint:quiet && pnpm test:quiet
Aggregates (multi-table records)
- Extends + extra fields — one primary table with optional joined fields (
OrgMemberRecord extends OrgMemberRowShape { userName?: string })
- Composition — multiple records wrapped in a container type (
TeamDashboard { team: TeamRecord; assetCount: number })
- Intersection — flat union of multiple row shapes (
AssetWithOwner = MediaAssetRecord & { ownerName: string })
- Use suffixes:
Aggregate, View, Dashboard (allowed by ESLint for standalone interfaces)
- The repository writes the SQL joins; the service receives the assembled aggregate
Cross-Domain Boundaries
Events are public nouns. Ports are public verbs. Errors are private semantics.
Event contracts
- Each domain has a public
events.ts at its root: packages/core/src/domains/<domain>/events.ts
- This is the ONLY file other domains may import from a sibling domain
- Contains: event type discriminants, typed payload shapes, version constants
- Internal payload definitions stay in
domain/*-events.ts; events.ts re-exports them
- ESLint + structural lint enforce this — all other cross-domain imports are blocked
Error architecture
- Rich typed ADT errors inside a domain (use
Either): yes
- Cross-domain import of another domain's errors: no
- Translate errors at seams — ports define their own consumer-facing error types
- Application layer composes domain errors into
CoreError
import { OrganizationNotFoundError } from '../../organization/domain/organization-errors.js'
type LoadOrganizationForAssetError =
| { _tag: 'OrganizationMissing' }
| { _tag: 'OrganizationUnavailable' }
Constants
Centralized in packages/contracts/src/constants.ts (import from @tx-agent-kit/contracts).
Never hardcode magic numbers in services — import from contracts.
Full domain layer guide
See packages/core/CLAUDE.md for detailed rules, examples, and patterns.