ADR-003: Domain package shape — domain / application / infrastructure / presentation
Context
Each bounded context in packages/<domain>/ must be:
- Reviewable on its own without touching other domains.
- Carve-out-ready when extraction triggers fire (ADR-001).
- Unit-testable without a database.
A flat src/ layout couples domain rules to the ORM and HTTP framework. Carving out a flat src/ domain later requires a rewrite.
Decision
Inside packages/<domain>/backend/:
domain/ pure business rules and value objects.
NO @nestjs/*, NO @prisma/client.
application/ use cases, application services, ports (interfaces).
Depends on domain/. Defines ports for infrastructure.
infrastructure/ adapters implementing ports — Prisma repos, HTTP clients,
Kafka producers, third-party SDKs.
presentation/ NestJS controllers, DTOs, guards. Translates HTTP into
use-case calls. NO business logic.
<domain>.module.ts wires ports → impls, registers controllers.
Alternatives considered
- Flat
src/per domain. Rejected — couples domain rules to NestJS + Prisma; makes carve-out a rewrite. - Per-feature folders (
payroll/run/,payroll/approve/) instead of horizontal layers. Rejected for the bedrock layers, but acceptable withinapplication/once a domain grows past a handful of use cases. - Sharing a single
src/layout across all domains by convention. Rejected — we want the layout enforced by lint, not by reviewer vigilance.
Consequences
Positive
- Carving out a domain into a Go service is a layer-by-layer port, not a rewrite.
domain/translates directly;application/is the use-case API the new service exposes;infrastructure/gets a different impl. - Pure-TS
domain/andapplication/are unit-testable with fakes in milliseconds. - New engineers learn one layout, work across all domains.
Negative
- A small domain has more folders than its code seems to justify. Pay-once for the structure; recoup on every refactor.
- Two levels of indirection (
use-case → port → repo) instead of "just call Prisma."
Follow-ups
- ESLint rule (custom, ships in
tooling/eslint/) forbids@prisma/clientand@nestjs/*inbackend/{domain,application}/**. pnpm gen:domain <name>generator scaffolds the structure correctly.- A separate
tests/directory hosts integration tests; pure unit tests live alongside their source.