Architecture Presets
Complete reference for all 7 architecture presets in Technical Debt Radar. Each preset generates dependency rules, layer mappings, and detection patterns tailored to a specific architecture style.
Architecture Presets
Architecture presets are templates that generate dependency rules based on your project's architecture pattern. When you set architecture: ddd in radar.yml and run radar init, the preset generates a complete rules.yml with rules that enforce DDD boundaries.
# radar.yml
architecture: ddd
radar init # Generates rules.yml with DDD rules
Technical Debt Radar ships with 7 presets:
| Preset | Architecture Style | Cross-Module Via |
|---|---|---|
ddd | Domain-Driven Design | contracts/ |
hexagonal | Hexagonal / Ports & Adapters | ports/ |
clean | Clean Architecture | interfaces/ |
layered | Traditional Layered | types/ |
mvc | Model-View-Controller | Direct (no isolation) |
event-driven | CQRS + Event Sourcing | events/ |
feature-module | NestJS Feature Modules | NestJS DI |
Tip: Not sure which preset to pick? Use
radar initwithout settingarchitecture-- the CLI auto-detects your pattern by scanning directory names and file conventions.
Combining Presets
You can combine presets when your codebase uses multiple patterns:
# radar.yml
architecture: [ddd, event-driven]
When combining presets:
- Architecture rules from all presets are merged
- Duplicate rules (same source/target/type) are deduplicated
- If the same dependency path has both
denyandallow,denywins - Runtime, reliability, and performance rules use the first preset's defaults (they are identical across presets)
Warning: Some combinations generate conflicting rules. The following are flagged as unusual:
ddd+mvc,hexagonal+mvc,clean+mvc,clean+layered.
How Presets Work
Each preset defines:
- Architecture rule templates -- dependency rules using placeholders like
{domain},{infrastructure} - Layer mapping -- maps placeholders to your layer names defined in
radar.yml - Shared defaults -- runtime rules, reliability rules, performance rules, gates, and scoring
When generating rules.yml, the engine:
- Reads your
layersfromradar.yml - Maps preset placeholders to your actual layer names
- Skips rules where a placeholder cannot be mapped (your project doesn't have that layer)
- Writes the resolved rules to
rules.yml
This means presets adapt to your directory structure. A DDD preset with layers named core and adapters works just as well as one with domain and infra.
DDD
Domain-Driven Design. The domain layer is pure -- it has no outward dependencies. Application layer orchestrates use cases. Infrastructure implements persistence and external integrations. API layer handles HTTP routing.
Dependency direction: API -> Application -> Domain; Infrastructure -> Domain
api ──────> application ──────> domain
^
infrastructure ───────────────────┘
Generated Rules
| Rule | Description |
|---|---|
deny: domain -> infrastructure | Domain must not depend on infrastructure |
deny: domain -> api | Domain must not depend on API/controllers |
deny: domain -> application | Domain must not import from application layer |
deny: api -> infrastructure | Controllers must use application layer, not infra directly |
deny: cross-module direct imports | Modules isolated from each other |
allow: cross-module through contracts | Cross-module via contracts only |
Layer Mapping
| Placeholder | Maps to |
|---|---|
{domain} | domain |
{api} | api |
{application} | application |
{infrastructure} | infrastructure |
Detection Patterns
The radar init auto-detector looks for: domain/, use-cases/, infra/, controllers/
Complete Example
# radar.yml
stack:
language: TypeScript
framework: NestJS
orm: Prisma
structure: modular-monolith
runtime: node
architecture: ddd
layers:
- name: api
path: "src/**/controllers/**"
- name: application
path: "src/**/use-cases/**"
- name: domain
path: "src/**/domain/**"
- name: infrastructure
path: "src/**/infra/**"
modules:
- name: billing
path: "src/billing/**"
- name: identity
path: "src/identity/**"
- name: orders
path: "src/orders/**"
data_volumes:
orders: L
events: XL
users: M
When to use DDD: Teams with clear domain boundaries, aggregate roots, value objects, and repository abstractions. Best for complex business logic where the domain model is the primary asset.
Hexagonal
Hexagonal Architecture (Ports & Adapters). The domain is the core. Ports define interfaces. Adapters implement those interfaces. The dependency rule: nothing in the core depends on anything outside.
Dependency direction: Adapters implement Ports; Application uses Ports and Domain
infrastructure ──> ports <── application ──> domain
(adapters) ^
Generated Rules
| Rule | Description |
|---|---|
deny: domain -> infrastructure | Domain must not depend on adapters |
deny: ports -> infrastructure | Ports must not depend on adapters |
deny: domain -> application | Domain is innermost -- no outward deps |
allow: infrastructure -> ports | Adapters may implement ports |
allow: application -> ports | Application may use ports |
allow: application -> domain | Application may depend on domain |
deny: cross-module direct imports | Module isolation |
allow: cross-module through ports | Cross-module via ports |
Layer Mapping
| Placeholder | Maps to |
|---|---|
{domain} | domain |
{ports} | ports |
{infrastructure} | infrastructure |
{application} | application |
Detection Patterns
The auto-detector looks for: ports/, adapters/, domain/
Complete Example
# radar.yml
stack:
language: TypeScript
framework: NestJS
orm: Prisma
runtime: node
architecture: hexagonal
layers:
- name: domain
path: "src/**/domain/**"
- name: ports
path: "src/**/ports/**"
- name: application
path: "src/**/application/**"
- name: infrastructure
path: "src/**/adapters/**"
modules:
- name: payments
path: "src/payments/**"
- name: users
path: "src/users/**"
data_volumes:
payments: L
users: M
When to use Hexagonal: Teams that want clear port interfaces for testability. The domain can be tested in complete isolation by mocking ports. Good for systems with multiple infrastructure adapters (e.g., switch between Redis and Memcached without touching domain code).
Clean
Clean Architecture (Uncle Bob). Four concentric layers: Entities (innermost), Use Cases, Interface Adapters, Frameworks & Drivers (outermost). Inner layers never depend on outer layers.
Dependency direction: Outer -> Inner (strictly)
frameworks ──> interfaces ──> use-cases ──> entities
(outermost) (innermost)
Generated Rules
| Rule | Description |
|---|---|
deny: domain -> application | Entities must not depend on use cases |
deny: domain -> interfaces | Entities must not depend on interface adapters |
deny: domain -> api | Entities must not depend on frameworks |
deny: application -> interfaces | Use cases must not depend on interface adapters |
deny: application -> api | Use cases must not depend on frameworks |
allow: interfaces -> application | Interface adapters may depend on use cases |
allow: interfaces -> domain | Interface adapters may depend on entities |
allow: api -> interfaces | Frameworks may depend on interface adapters |
deny: cross-module direct imports | Module isolation |
allow: cross-module through interfaces | Cross-module via interfaces |
Layer Mapping
| Placeholder | Maps to |
|---|---|
{domain} | domain (entities) |
{application} | application (use cases) |
{interfaces} | api (interface adapters) |
{api} | infrastructure (frameworks & drivers) |
Note: The Clean Architecture preset maps
{interfaces}toapiand{api}toinfrastructureby default. This works when your layers are nameddomain,application,api,infrastructure. If you use different names likeentities,use-cases,adapters,frameworks, the engine resolves placeholders via the layer mapping.
Detection Patterns
The auto-detector looks for: entities/, interfaces/, frameworks/, use-cases/
Complete Example
# radar.yml
stack:
language: TypeScript
framework: Fastify
orm: Prisma
runtime: node
architecture: clean
layers:
- name: domain
path: "src/domain/**"
- name: application
path: "src/use-cases/**"
- name: api
path: "src/adapters/**"
- name: infrastructure
path: "src/infrastructure/**"
modules:
- name: inventory
path: "src/inventory/**"
- name: shipping
path: "src/shipping/**"
data_volumes:
inventory_items: L
shipments: XL
When to use Clean: Teams building applications where the domain logic is complex and must be isolated from framework choices. The strict layering ensures that swapping a framework (Express to Fastify) or ORM (Prisma to Drizzle) only requires changes in the outermost layers.
Layered
Traditional three-tier layered architecture. Presentation at the top, business logic in the middle, data access at the bottom. Dependencies flow strictly downward.
Dependency direction: Presentation -> Business -> Data Access
presentation ──> business ──> data-access
| ^
└────────────────┘ (allowed)
Generated Rules
| Rule | Description |
|---|---|
deny: presentation -> data-access | Presentation cannot skip business layer |
deny: data-access -> presentation | Data access cannot depend on presentation |
deny: data-access -> business | Data access cannot depend on business logic |
allow: presentation -> business | Presentation may depend on business logic |
allow: business -> data-access | Business logic may depend on data access |
deny: cross-module direct imports | Module isolation |
allow: cross-module through contracts | Cross-module via contracts |
Layer Mapping
| Placeholder | Maps to |
|---|---|
{presentation} | api |
{business} | application |
{data-access} | infrastructure |
Detection Patterns
The auto-detector looks for: controllers/, services/, repositories/
Complete Example
# radar.yml
stack:
language: TypeScript
framework: Express
orm: Sequelize
runtime: node
architecture: layered
layers:
- name: api
path: "src/controllers/**"
- name: application
path: "src/services/**"
- name: infrastructure
path: "src/repositories/**"
modules:
- name: core
path: "src/**"
data_volumes:
users: M
orders: L
products: M
When to use Layered: Teams with straightforward CRUD applications where the primary concern is maintaining a clean separation between HTTP handling, business rules, and data access. Simpler than DDD or Clean but still prevents spaghetti dependencies.
MVC
Model-View-Controller. Models are independent (no upward dependencies). Controllers orchestrate models and views. Views depend on models for data rendering.
Dependency direction: Controllers -> Models; Views -> Models; Controllers -> Views
controllers ──> models
| ^
v |
views ─────────┘
Generated Rules
| Rule | Description |
|---|---|
deny: models -> controllers | Models must not depend on controllers |
deny: models -> views | Models must not depend on views |
deny: controllers -> __db__ | Controllers must not access the database directly |
allow: controllers -> models | Controllers may depend on models |
allow: views -> models | Views may depend on models |
deny: cross-module direct imports | Module isolation |
allow: cross-module through contracts | Cross-module via contracts |
Layer Mapping
| Placeholder | Maps to |
|---|---|
{models} | domain |
{controllers} | api |
{views} | views |
Detection Patterns
The auto-detector looks for: models/, views/, controllers/
Complete Example
# radar.yml
stack:
language: TypeScript
framework: Express
orm: Mongoose
runtime: node
architecture: mvc
layers:
- name: domain
path: "src/models/**"
- name: api
path: "src/controllers/**"
- name: views
path: "src/views/**"
modules:
- name: core
path: "src/**"
data_volumes:
users: M
posts: L
When to use MVC: Teams building traditional server-rendered applications or REST APIs with simple domain logic. The MVC pattern is well-understood and works well for smaller applications. Not recommended for complex domain logic -- consider DDD or Clean instead.
Event-Driven
CQRS + Event-Driven Architecture. Commands and queries are separated. Handlers process commands/queries and emit events. Events flow through an event bus. Handlers must not call other handlers directly.
Dependency direction: Handlers -> Events; Handlers -> Services; Commands -> Services; Queries -> Infrastructure
commands ──> services
queries ──> infrastructure
handlers ──> events
handlers ──> services
Generated Rules
| Rule | Description |
|---|---|
deny: handlers -> handlers | Handlers must not directly import other handlers |
deny: commands -> queries | CQRS: commands must not depend on queries |
deny: events -> handlers | Events must not know their subscribers |
deny: services -> infrastructure | Services must not depend on infrastructure directly |
allow: handlers -> events | Handlers may emit events |
allow: handlers -> services | Handlers may use services |
allow: commands -> services | Commands may use services |
allow: queries -> infrastructure | Queries may access infrastructure for reads |
deny: cross-module direct imports | Module isolation |
allow: cross-module through events | Cross-module via events |
Layer Mapping
| Placeholder | Maps to |
|---|---|
{handlers} | handlers |
{commands} | commands |
{queries} | queries |
{events} | events |
{services} | application |
{infrastructure} | infrastructure |
Detection Patterns
The auto-detector looks for: commands/, queries/, handlers/, events/
Complete Example
# radar.yml
stack:
language: TypeScript
framework: NestJS
orm: Prisma
structure: modular-monolith
runtime: node
architecture: event-driven
layers:
- name: commands
path: "src/**/commands/**"
- name: queries
path: "src/**/queries/**"
- name: handlers
path: "src/**/handlers/**"
- name: events
path: "src/**/events/**"
- name: application
path: "src/**/services/**"
- name: infrastructure
path: "src/**/infra/**"
modules:
- name: orders
path: "src/orders/**"
- name: inventory
path: "src/inventory/**"
- name: notifications
path: "src/notifications/**"
data_volumes:
orders: L
events: XXL
inventory: M
When to use Event-Driven: Teams building event-sourced systems, CQRS architectures, or microservices that communicate via events. The preset enforces that commands and queries stay separate, and that modules communicate only through the event bus.
Feature-Module
The most common NestJS pattern. Each feature is a single folder containing a controller, service, entity, module file, and DTOs. No separate architectural layers. Modules communicate through NestJS dependency injection.
Key difference from other presets: Feature-module has relaxed cross-module rules. Imports of modules, entities, interfaces, decorators, DTOs, and shared code across module boundaries are allowed. Only service-to-service and controller-to-controller cross-module imports are violations.
Generated Rules
| Rule | Description |
|---|---|
deny: cross-module direct imports | Base deny for cross-module imports |
allow: cross-module through *.module.{ts,js} | NestJS module wiring is allowed |
allow: cross-module through *.entity.{ts,js} | Shared entity imports allowed |
allow: cross-module through *.interface.{ts,js} | Contract interface imports allowed |
allow: cross-module through *.decorator.{ts,js} | Shared decorator imports allowed |
allow: cross-module through **/dto/** | DTO imports across modules allowed |
allow: cross-module through **/shared/** | Shared folder imports allowed |
Layer Mapping
| Placeholder | Maps to |
|---|---|
{api} | api |
{application} | application |
{infrastructure} | infrastructure |
{config} | config |
Detection Patterns
The auto-detector looks for: co-located .controller.ts + .service.ts + .module.ts files
Complete Example
# radar.yml
stack:
language: TypeScript
framework: NestJS
orm: TypeORM
runtime: node
architecture: feature-module
layers:
- name: controllers
path: "src/**/*.controller.ts"
- name: services
path: "src/**/*.service.ts"
- name: entities
path: "src/**/*.entity.ts"
- name: shared
path: "src/shared/**"
modules:
- name: users
path: "src/users/**"
- name: products
path: "src/products/**"
- name: orders
path: "src/orders/**"
data_volumes:
users: M
products: M
orders: L
When to use Feature-Module: Teams using the default NestJS project structure where each feature folder owns all its files. This is the most pragmatic choice for NestJS projects that don't need strict DDD boundaries. Works well up to ~20 modules before you might want to consider DDD.
Shared Rule Defaults
All presets share the same defaults for non-architecture rules. These are applied to every generated rules.yml:
Default Runtime Rules
runtime_rules:
block:
- sync-fs-in-handler
- sync-crypto
- sync-compression
- redos-vulnerable-regex
- busy-wait-loop
- unhandled-promise
warn:
- unbounded-json-parse
- large-json-stringify
- cpu-heavy-loop-in-handler
- unbounded-array-operation
- dynamic-buffer-alloc
Default Reliability Rules
reliability_rules:
block:
- unhandled-promise-rejection
warn:
- missing-try-catch
- external-call-no-timeout
- retry-without-backoff
- empty-catch-block
- missing-error-logging
- transaction-no-timeout
- missing-null-guard
Default Performance Rules
performance_rules:
detect:
- unbounded-find-many
- find-many-no-where
- nested-include-large-relation
- n-plus-one-query
- fetch-all-filter-in-memory
- missing-pagination-endpoint
- unfiltered-count-large-table
- raw-sql-no-limit
Default Gates
gates:
block_merge:
- architecture_violations > 0
- circular_dependencies_introduced > 0
- runtime_risk_critical > 0
- reliability_critical > 0
- critical_performance_risk > 0
- debt_delta_score > 15
warn:
- complexity_increase > 5
- debt_delta_score > 8
You can override any of these in your rules.yml after generation.