Clean Architecture — TypeScript
Apply Clean Architecture (Robert C. Martin), DDD tactical patterns, Hexagonal/Explicit Architecture (Herberto Graca), and Clean Code practices to TypeScript codebases. 76 rules across 11 categories.
Guardrails — never violate these
- Never import outer layers from inner layers — domain imports nothing; application imports only domain
- Never put ORM decorators or Prisma types on domain entities — use mappers in infrastructure
- Never return
nullfor domain errors — use a discriminatedResult<T, E>return. In this repo the result branch usesok: true | false; ifEis a rich error union, give each error variant an_tagfor exhaustive handling. - Never use
as anyor@ts-ignorewithout a documented justification comment - Never dispatch domain events before persistence — pull events AFTER
save(), dispatch AFTER commit - Never share one type across domain/API/DB layers — accept cross-layer duplication, each model serves a different master
- Never use barrel files (
index.tswithexport *) in application code — use direct imports - Never validate inside domain or use cases — parse at the adapter boundary with Zod, trust inside
- Always use
#private fields on entities (runtime encapsulation) — not theprivatekeyword - Always separate
Entity.create()(validates, emits events) fromEntity.reconstitute()(loads from DB, no events) - Always wire dependencies in a single composition root (
main.ts) — the only file that knows all concretions - Always use
import typefor cross-layer type imports — enableverbatimModuleSyntax: true - Always require
strict: true+noUncheckedIndexedAccess+exactOptionalPropertyTypesin tsconfig
Trigger boundary
Use this skill when:
- Designing new modules, services, or bounded contexts
- Reviewing code for architectural or dependency-direction violations
- Refactoring coupled systems toward cleaner layer separation
- Implementing entities, value objects, aggregates, or domain events
- Auditing tsconfig strictness, naming quality, or function design
- Choosing between layered architecture and vertical slices
Do NOT use this skill when:
- The task is purely about React/Vue/Angular component rendering (use framework skill)
- The task is about build tooling, CI/CD, or deployment only
- The task is a quick script or throwaway prototype with no long-term maintenance
Mode detection
Before starting, determine your mode:
| Signal | Mode | Behavior |
|---|---|---|
| "Design", "architect", "structure", "plan" | Designing | Propose layer structure, reference decision-tables.md |
| "Review", "audit", "check", "assess" | Reviewing | Report findings with severity, never auto-fix, block on guardrail violations |
| "Implement", "write", "create", "add", "build" | Implementing | Write code following loaded references, verify with typecheck |
| "Refactor", "migrate", "extract", "move" | Refactoring | Apply minimal targeted changes, preserve behavior, verify tests pass |
| Ambiguous | Ask | Clarify with the user before proceeding |
Required workflow
Step 1 — Classify the task
Identify the primary category and one adjacent category:
| Category | References to load | When |
|---|---|---|
| Dependency Direction | dep-inward-only, dep-interface-ownership, dep-dry-vs-duplication | Import direction issues, layer coupling, DRY vs duplication |
| Entity Design | entity-rich-not-anemic, entity-aggregate-roots, entity-create-reconstitute | Domain modeling, invariants, aggregates, factories |
| Use Case Isolation | usecase-orchestrates-not-implements, usecase-input-output-ports | Use case design, port definitions, orchestration |
| Clean Code | code-error-handling, code-parse-dont-validate, code-objects-vs-data | Naming, functions, error handling, validation |
| TypeScript Strictness | ts-strict-config, ts-branded-types, ts-result-type, ts-lsp-performance | Type safety, config, branded types, LSP speed |
| Architecture Patterns | pattern-cqrs-separation, pattern-domain-events, pattern-vertical-slices | CQRS, events, vertical slices |
| Boundaries | bound-composition-root, adapt-explicit-architecture, adapt-controller-thin | Composition root, ports, adapters, controllers |
| Framework Isolation | frame-domain-purity, frame-orm-in-infrastructure | ORM leaks, framework coupling |
| Testing | test-testing-pyramid, test-layer-isolation | Test strategy, pyramid alignment |
Steering note: Most tasks span two categories. Load the primary reference plus one adjacent. If uncertain, scan the category table for keywords matching the user's request.
Step 2 — Load references
Read the reference file(s) from references/ identified in Step 1. Read the full file — do not skim. If a loaded reference example conflicts with the guardrails or repo conventions in this file, follow this file.
If the task involves existing code, also read:
- The project's
tsconfig.json(compare against ts-strict-config.md) - The project's layer structure (identify domain/, application/, infrastructure/ or equivalents)
Steering note: Always check if the project already has an AGENTS.md or architecture docs. Adapt your output to match the project's existing naming conventions and layer structure.
Step 3 — Execute the task
Apply patterns from loaded references. Follow mode-specific behavior:
In designing mode:
- Propose folder structure per comp-screaming-architecture.md (package-by-component)
- Define port interfaces in application/domain layer, implementations in infrastructure
- Load decision-tables.md for architecture selection based on domain complexity
In implementing mode:
- Entities:
#private fields,create()+reconstitute()factories,pullDomainEvents() - Use cases: constructor-injected ports,
Result<T,E>returns, orchestrate-not-implement - Adapters: thin controllers (parse, delegate, respond), Zod schemas at boundary
import typefor all type-only imports across layers
In reviewing mode:
- Check every guardrail (top of this file) — violations are automatically CRITICAL
- Check dependency direction: inner layers must never import from outer layers
- Check entity design: no anemic models, no ORM decorators, proper factories
- Flag severity: CRITICAL (guardrail), WARNING (quality degradation), INFO (polish)
In refactoring mode:
- Apply minimal changes. One refactoring at a time.
- Preserve public API contracts. Preserve test behavior.
- Load decision-tables.md for anti-pattern recognition table
- For low-complexity requests, the smallest acceptable boundary split is: pure domain logic, one application entry point, boundary parsing in adapters, and one composition root.
Steering note: Never apply Clean Architecture to a simple CRUD app that doesn't need it. Check domain complexity first. For LOW complexity, vertical slices + Zod + Result types are sufficient. If the user explicitly asks for a refactor toward cleaner boundaries anyway, do the smallest useful version: isolate pure domain logic, keep one composition root, and move boundary parsing to adapters without forcing extra ceremony.
Step 4 — Verify
After making changes:
- Read the project's scripts or workspace docs first, then run the strongest project-native typecheck command available (
npm run typecheck,pnpm typecheck,tsc --noEmit -p tsconfig.json, ornpx tsc --noEmitonly if the repo installs TypeScript locally and that command works here) - Run tests if configured (
npm test,pnpm test, or project equivalent). If no tests are configured, state that explicitly and fall back to build + typecheck instead of pretending a test step exists. - Check imports: no outer-to-inner violations
- Check entities:
#private fields,create/reconstituteseparation - Check boundaries: ports in consuming layer, implementations in infrastructure
If the project contains TSX but the task is architecture-only, either install the required React runtime/types before typechecking or scope the verification command to the non-UI packages/modules you actually changed. State which path you took.
Step 5 — Deliver
- Designing: Output folder structure, port interfaces, layer diagram
- Implementing: Output complete, compilable code with explicit return types
- Reviewing: Output structured findings list with severity, file, line references
- Refactoring: Output targeted diffs with before/after
Steering note: Always produce a deliverable — code, findings list, or structure. Never end with only commentary.
Common mistakes to avoid
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Applying full Clean Architecture to a simple CRUD app | Over-engineering; 4+ layers for no benefit | Use vertical slices; add layers only when complexity emerges |
| Sharing one type across all layers ("DRY") | DB schema change breaks API; passwordHash leaks | Per-layer models with mappers — each has different change reason |
| Mocking domain entities in tests | Entities are pure — mocking defeats the purpose | Test entities directly; mock only ports in use case tests |
| Dispatching events before commit | Event published, DB rolled back = inconsistency | Pull events after save, dispatch after commit |
Using private keyword on entity fields | Compile-time only — bypassable with as any | Use # private fields for runtime encapsulation |
| Putting Zod validation inside domain/use cases | Wrong layer — parsing belongs at the adapter boundary | Parse at HTTP boundary with Zod; domain receives trusted types |
Using barrel index.ts in app code | Cascade loads 3x+ modules, causes circular deps | Direct imports from source files |
| One constructor for both creation and DB loading | Duplicate event emission on every load; or skipped validation | create() for new entities, reconstitute() for DB loads |
| Flagging absence of patterns as violations | Optional patterns (events, CQRS) are not mandatory | Only audit what exists — don't flag absence of optional patterns |
Reference routing
All references live in references/. Load by category:
Additional supporting files:
- _sections.md — Category definitions and impact levels
- assets/templates/_template.md — Template for adding new rules
Guardrails — repeated for recall
- Source dependencies point inward only — domain never imports outer layers
- Entities use
#private fields,create()+reconstitute()factories,Result<T,E>returns using the repo's discriminated union convention - Parse at boundary with Zod — never validate inside domain
- Events dispatched AFTER persistence — never before commit
- Accept cross-layer DTO duplication — DRY ends at the layer boundary
main.tsis the only composition root — the only file that knows all concretions