Configuration

Volume Configuration

How to declare data volumes for database entities and how volume tiers affect performance rule severity, merge gates, and scan results in Technical Debt Radar.

Volume Configuration

Volume declarations tell Technical Debt Radar how much data each database entity holds. This drives severity scaling for performance rules -- an unbounded findMany() on a 500-row config table is fine, but the same pattern on a 10-million-row events table is a production incident waiting to happen.

# radar.yml
data_volumes:
  orders: L
  events: XL
  users: M
  companies: S
  measurements: XXL

Volume Tiers

TierRow EstimateRow RangeEffect on Performance Rules
S< 10K0 -- 10,000Most performance warnings suppressed
M10K -- 100K10,000 -- 100,000Warnings shown in PR comment
L100K -- 1M100,000 -- 1,000,000Severity elevated to Critical
XL1M -- 50M1,000,000 -- 50,000,000Blocks merge
XXL> 50M50,000,000+Blocks merge

Note: Volume tiers are estimates, not exact row counts. Pick the tier that reflects your entity's expected scale in production, not its current dev environment size. An events table with 1,000 rows today but expected to grow to millions should be declared as XL.


How Volumes Affect Scan Results

Performance Rule Severity Scaling

Each performance rule's severity is adjusted based on the volume of the entity involved:

Performance RuleSMLXLXXL
unbounded-find-manySuppressedWarnCriticalBlockBlock
find-many-no-whereSuppressedWarnCriticalBlockBlock
nested-include-large-relationSuppressedWarnCriticalBlockBlock
n-plus-one-queryWarnWarnCriticalBlockBlock
fetch-all-filter-in-memorySuppressedWarnCriticalBlockBlock
missing-pagination-endpointSuppressedWarnWarnCriticalBlock
unfiltered-count-large-tableSuppressedSuppressedWarnCriticalBlock
raw-sql-no-limitWarnWarnCriticalBlockBlock

How it works in practice:

// This code queries the "orders" entity (declared as L)
const orders = await prisma.order.findMany();
//                                 ^^^^^^^^
// unbounded-find-many on L entity → CRITICAL (blocks merge)
// Same pattern on "companies" entity (declared as S)
const companies = await prisma.company.findMany();
//                                      ^^^^^^^^
// unbounded-find-many on S entity → SUPPRESSED (not reported)

Gate Threshold Interaction

Volume-adjusted severity feeds into the gate metrics:

  • A finding elevated to Critical counts toward critical_performance_risk
  • A finding elevated to Block counts toward critical_performance_risk
  • Gate condition critical_performance_risk > 0 blocks the merge

This means a single unbounded query on an XL table can block a PR, even if the default performance gate threshold is generous.


Declaring Volumes

Manual Declaration

Add data_volumes to your radar.yml with entity names as keys and volume tiers as values:

data_volumes:
  users: M
  orders: L
  order_items: L
  events: XL
  audit_logs: XXL
  measurements: XXL
  categories: S
  settings: S
  roles: S
  permissions: S

Entity names should match your ORM model names (in snake_case). The policy engine matches these against ORM queries detected in your code.

Auto-Detection

Run radar init to auto-detect volumes from your ORM schema. The estimator supports:

ORMSchema SourceDetection Method
Prismaprisma/schema.prismaParses model blocks, counts relations, checks @@index on createdAt
TypeORM*.entity.ts filesParses @Entity() decorators, counts relation decorators, checks @Index on timestamps
MikroORM*.entity.ts filesParses @Entity({ tableName }), counts relations
Sequelize*.model.ts filesParses class extends Model and sequelize.define(), checks indexes
Mongoose*.model.ts filesParses mongoose.model(), checks .index() definitions
Drizzleschema.ts / *.table.tsParses pgTable(), mysqlTable(), sqliteTable() calls

Heuristic Estimation

When auto-detecting, the estimator uses entity naming patterns as a starting point:

Name PatternDefault TierReasoning
measurement*, metric*, telemetry*, trace*XXLTime-series data, append-only, grows unbounded
event*, log*, audit*, activity*, notification*XLEvent streams, high write throughput
order*, invoice*, payment*, subscription*, transaction*LTransactional data, grows with business volume
user*, account*, team*, org*, company*MEntity data, bounded by customer count
config*, setting*, category*, type*, role*, permission*SReference data, rarely changes
Everything elseSConservative default

Adjustments applied after heuristic estimation:

SignalAdjustment
4+ relation fields on the modelBump one tier (S -> M, M -> L, etc.)
Index on createdAt or timestampBump to at least XL (suggests time-series pattern)

Tip: Always review auto-detected volumes. The heuristic is a starting point -- your production data patterns may differ. A widget table with millions of rows won't be detected as XL by name alone.


Volume-Aware Examples

Example 1: N+1 Query on Different Volumes

Consider this code:

// src/orders/use-cases/list-orders.ts
const users = await prisma.user.findMany({ where: { active: true } });
for (const user of users) {
  const orders = await prisma.order.findMany({
    where: { userId: user.id },
  });
  // process orders...
}

This is an N+1 query. The scan result depends on volumes:

users volumeorders volumeFindingSeverity
SSN+1 query detectedWarn
MLN+1 query on L entityCritical
MXLN+1 query on XL entityBlock

Example 2: Missing Pagination

// src/events/controllers/events.controller.ts
@Get()
async listEvents() {
  return this.eventsService.findAll();
}
// src/events/services/events.service.ts
async findAll() {
  return this.prisma.event.findMany();
}

Two issues detected, both volume-dependent:

events volumemissing-pagination-endpointunbounded-find-many
SSuppressedSuppressed
MWarnWarn
LWarnCritical
XLCriticalBlock
XXLBlockBlock

Example 3: Fetch-All and Filter in Memory

// Bad: fetches all orders, filters in application code
const allOrders = await prisma.order.findMany();
const recentOrders = allOrders.filter(
  (o) => o.createdAt > thirtyDaysAgo,
);
orders volumeFindingSeverityFix
Sfetch-all-filter-in-memorySuppressed--
Mfetch-all-filter-in-memoryWarnAdd where clause
Lfetch-all-filter-in-memoryCriticalAdd where clause
XLfetch-all-filter-in-memoryBlockAdd where clause

The fix is the same regardless of volume:

// Good: filter at the database level
const recentOrders = await prisma.order.findMany({
  where: { createdAt: { gte: thirtyDaysAgo } },
});

Entity Mapping

The volume estimator maps entity names from your data_volumes to ORM queries using snake_case normalization:

data_volumes keyMatches these Prisma modelsMatches these TypeORM entities
ordersOrderOrderEntity, @Entity('orders')
order_itemsOrderItemOrderItemEntity, @Entity('order_items')
user_eventsUserEventUserEventEntity

The matching is case-insensitive and handles common naming conventions (PascalCase models, snake_case tables, pluralization).

Warning: If your ORM model name doesn't match your data_volumes key after snake_case conversion, the volume tier won't be applied. Ensure your keys match your actual table/model names.


Undeclared Entities

When a performance rule detects a query on an entity that isn't declared in data_volumes, the default tier is S (suppressed for most rules). This means:

  • Undeclared entities won't block merges for performance issues
  • You'll miss real problems on high-volume tables you forgot to declare

Run radar volumes to see which entities are queried in your codebase but missing from data_volumes:

radar volumes --check
# Output:
# Declared: orders (L), events (XL), users (M)
# Missing:  invoices (queried in 3 files), audit_logs (queried in 7 files)
# Suggestion: Add these to data_volumes in radar.yml

Best Practices

  1. Declare all entities that could be large. Start with auto-detection, then review and adjust.

  2. Use production-scale estimates. A table with 100 rows in dev but 10 million in production should be XL.

  3. Reassess quarterly. As your product grows, entities move up tiers. A users table at M today might be L next quarter.

  4. Err on the side of larger tiers. An L declaration on an entity that's actually M produces extra warnings. An S declaration on an entity that's actually XL silently misses critical issues.

  5. Pair with data_volumes exceptions for legitimate patterns:

# radar.yml
data_volumes:
  events: XL

# rules.yml
exceptions:
  - rule: "unbounded-find-many"
    file: "src/admin/services/event-export.service.ts"
    expires: "2026-06-01"
    reason: "Admin-only export with streaming — not a production risk"
Technical Debt Radar Documentation