Code Review — TypeScript DDD
Layer dependency rule (check first — any violation is a [BLOCKER])
Domain → imports nothing outside domain/
Application → imports domain only
Infrastructure → imports domain + application interfaces
Presentation → imports application only
Checklist by layer
Domain (src/*/domain/)
- No framework imports (NestJS, TypeORM, Prisma, Express)
- Aggregates enforce invariants — business rules in aggregate, not handlers
- Value Objects are immutable (all properties
readonly, no setters) - Meaningful state changes emit domain events
- Repository interfaces use domain types only (no ORM entities, no raw strings)
- Flag anemic model if aggregate has only getters and no behavior methods
Application (src/*/application/)
- Handlers orchestrate only — no
if (order.status === 'PENDING')domain logic - Commands return void or ID, never a domain aggregate
- Queries use read models, not domain aggregates
- No infrastructure imports — depend on interfaces only
Infrastructure (src/*/infrastructure/)
- Mapper exists between ORM model and domain aggregate
reconstituteused for loading from DB, not the public constructor- Repository class declares
implements IXxxRepository - No business logic — pure I/O
Presentation (src/*/presentation/)
- Controllers validate input, call use case, map to response only
- Request/response DTOs separate from domain DTOs
- Domain exceptions mapped to HTTP status codes here, not in use case
Severity tags
- [BLOCKER] — Layer violation, missing invariant, data loss, security issue
- [BUG] — Incorrect behavior, wrong type, unhandled null/undefined
- [DESIGN] — Anemic model, fat handler, missing domain event
- [SIMPLIFY] — Unnecessary complexity or duplication
- [NIT] — Minor style issue (only mention if it genuinely confuses readers)
DDD-specific red flags (always call these out)
customerId: stringin a domain method — should beCustomerIdvalue object- One command modifying two separate aggregate roots
- Application service calling another application service
- Domain event handled in same transaction without an event bus
- Loading from DB via the creation constructor (should use
reconstitute)
Output format
Numbered list, most critical first. Each finding: [TAG] Title → location + problem + fix.