Configuration

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:

PresetArchitecture StyleCross-Module Via
dddDomain-Driven Designcontracts/
hexagonalHexagonal / Ports & Adaptersports/
cleanClean Architectureinterfaces/
layeredTraditional Layeredtypes/
mvcModel-View-ControllerDirect (no isolation)
event-drivenCQRS + Event Sourcingevents/
feature-moduleNestJS Feature ModulesNestJS DI

Tip: Not sure which preset to pick? Use radar init without setting architecture -- 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 deny and allow, deny wins
  • 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:

  1. Architecture rule templates -- dependency rules using placeholders like {domain}, {infrastructure}
  2. Layer mapping -- maps placeholders to your layer names defined in radar.yml
  3. Shared defaults -- runtime rules, reliability rules, performance rules, gates, and scoring

When generating rules.yml, the engine:

  1. Reads your layers from radar.yml
  2. Maps preset placeholders to your actual layer names
  3. Skips rules where a placeholder cannot be mapped (your project doesn't have that layer)
  4. 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

RuleDescription
deny: domain -> infrastructureDomain must not depend on infrastructure
deny: domain -> apiDomain must not depend on API/controllers
deny: domain -> applicationDomain must not import from application layer
deny: api -> infrastructureControllers must use application layer, not infra directly
deny: cross-module direct importsModules isolated from each other
allow: cross-module through contractsCross-module via contracts only

Layer Mapping

PlaceholderMaps 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

RuleDescription
deny: domain -> infrastructureDomain must not depend on adapters
deny: ports -> infrastructurePorts must not depend on adapters
deny: domain -> applicationDomain is innermost -- no outward deps
allow: infrastructure -> portsAdapters may implement ports
allow: application -> portsApplication may use ports
allow: application -> domainApplication may depend on domain
deny: cross-module direct importsModule isolation
allow: cross-module through portsCross-module via ports

Layer Mapping

PlaceholderMaps 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

RuleDescription
deny: domain -> applicationEntities must not depend on use cases
deny: domain -> interfacesEntities must not depend on interface adapters
deny: domain -> apiEntities must not depend on frameworks
deny: application -> interfacesUse cases must not depend on interface adapters
deny: application -> apiUse cases must not depend on frameworks
allow: interfaces -> applicationInterface adapters may depend on use cases
allow: interfaces -> domainInterface adapters may depend on entities
allow: api -> interfacesFrameworks may depend on interface adapters
deny: cross-module direct importsModule isolation
allow: cross-module through interfacesCross-module via interfaces

Layer Mapping

PlaceholderMaps to
{domain}domain (entities)
{application}application (use cases)
{interfaces}api (interface adapters)
{api}infrastructure (frameworks & drivers)

Note: The Clean Architecture preset maps {interfaces} to api and {api} to infrastructure by default. This works when your layers are named domain, application, api, infrastructure. If you use different names like entities, 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

RuleDescription
deny: presentation -> data-accessPresentation cannot skip business layer
deny: data-access -> presentationData access cannot depend on presentation
deny: data-access -> businessData access cannot depend on business logic
allow: presentation -> businessPresentation may depend on business logic
allow: business -> data-accessBusiness logic may depend on data access
deny: cross-module direct importsModule isolation
allow: cross-module through contractsCross-module via contracts

Layer Mapping

PlaceholderMaps 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

RuleDescription
deny: models -> controllersModels must not depend on controllers
deny: models -> viewsModels must not depend on views
deny: controllers -> __db__Controllers must not access the database directly
allow: controllers -> modelsControllers may depend on models
allow: views -> modelsViews may depend on models
deny: cross-module direct importsModule isolation
allow: cross-module through contractsCross-module via contracts

Layer Mapping

PlaceholderMaps 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

RuleDescription
deny: handlers -> handlersHandlers must not directly import other handlers
deny: commands -> queriesCQRS: commands must not depend on queries
deny: events -> handlersEvents must not know their subscribers
deny: services -> infrastructureServices must not depend on infrastructure directly
allow: handlers -> eventsHandlers may emit events
allow: handlers -> servicesHandlers may use services
allow: commands -> servicesCommands may use services
allow: queries -> infrastructureQueries may access infrastructure for reads
deny: cross-module direct importsModule isolation
allow: cross-module through eventsCross-module via events

Layer Mapping

PlaceholderMaps 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

RuleDescription
deny: cross-module direct importsBase 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

PlaceholderMaps 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.

Technical Debt Radar Documentation