Back to Blog

Software Architecture Review: ADRs, Technical Debt, and Fitness Functions

Establish architecture governance with Architecture Decision Records (ADRs), technical debt tracking, fitness functions for continuous validation, and arc42 documentation templates.

Viprasol Tech Team
July 24, 2026
12 min read

Software Architecture Review: ADRs, Technical Debt, and Fitness Functions

Architecture decisions made without documentation get forgotten, relitigated, and reversed. A team spends a week evaluating PostgreSQL vs. MongoDB, chooses PostgreSQL for strong consistency guarantees, then eighteen months later a new engineer asks "why are we using Postgres? MongoDB would be simpler for this use case." Without a record of the original decision and its context, the debate starts over.

Architecture governance doesn't require a separate Architecture Board or heavyweight RFC processes. It requires three lightweight practices: Architecture Decision Records (ADRs) to document decisions, technical debt registers to make tradeoffs visible, and fitness functions to automatically validate that the architecture stays healthy as the codebase evolves.


Architecture Decision Records (ADRs)

An ADR is a short document (one to two pages) that captures: the context, the decision made, the options considered, the tradeoffs, and the consequences. It lives in the repository alongside the code it describes.

ADR Template

# ADR-0014: Use PostgreSQL Row-Level Security for Multi-Tenancy

**Status:** Accepted  
**Date:** 2026-03-15  
**Deciders:** Backend team, CTO  
**Technical Story:** JIRA-2847 โ€” Multi-tenant data isolation

---

## Context

We are building a multi-tenant SaaS product where each tenant's data must
be strictly isolated. We evaluated three approaches:

1. **Separate databases per tenant** โ€” strongest isolation, highest operational cost
2. **Separate schemas per tenant** โ€” good isolation, schema migration complexity
3. **Shared schema with Row-Level Security** โ€” operational simplicity, adequate isolation

We have ~50 tenants today, projecting 500 within 18 months.

---

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Decision

Use PostgreSQL Row-Level Security (RLS) with a shared schema.

Set tenant context via SET LOCAL app.current_tenant_id = $1 at the start of each request, inside a transaction. RLS policies enforce that all queries automatically filter to the current tenant.


Options Considered

OptionProsCons
Separate databasesPerfect isolation, easy backup per tenantOps complexity scales with tenant count
Separate schemasGood isolation, per-tenant migrations possibleMigration fan-out problem at 500 tenants
Shared schema + RLSSimple ops, single migration pathRLS policy bugs can cause data leakage

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Consequences

Positive:

  • Single migration run affects all tenants
  • Simple connection pooling (one pool for all tenants)
  • Easy to add a new tenant (just insert a row)

Negative:

  • A bug in RLS policies could expose cross-tenant data โ€” requires thorough testing
  • SET LOCAL must be called in every transaction; framework enforcement needed
  • Complex analytics queries must explicitly handle tenant_id

Mitigations:

  • Automated test suite: integration tests verify RLS isolation per-tenant
  • Middleware enforces SET LOCAL before every database operation
  • Code review checklist: "Does this query bypass RLS?"

Links


### ADR File Structure

docs/ decisions/ 0001-use-next-js-for-frontend.md 0002-fastify-over-express.md 0003-postgresql-primary-database.md 0004-aws-ecs-fargate-deployment.md ... 0014-rls-multi-tenancy.md README.md โ† Index of all ADRs with one-line summaries


### ADR Status Values

- **Proposed** โ€” Under discussion, not yet decided
- **Accepted** โ€” Decision made and in effect
- **Deprecated** โ€” No longer applies (context changed)
- **Superseded by ADR-XXXX** โ€” Replaced by a newer decision

### CLI Tool for ADR Management

```bash
# Install adr-tools
npm install -g adr-tools

# Initialize ADR directory
adr init docs/decisions

# Create new ADR (auto-numbered)
adr new "Use Redis for session storage"
# Creates: docs/decisions/0015-use-redis-for-session-storage.md

# List all ADRs
adr list

# Link related ADRs
adr link 15 Supersedes 8

Technical Debt Register

Technical debt without visibility is debt you can't pay down. A technical debt register makes it explicit: what's the debt, who owns it, what's the impact, and when will it be addressed.

# Technical Debt Register

Last updated: 2026-07-01

## Format
Each item: ID | Component | Description | Impact | Owner | Priority | Target Quarter

---

Active Debt

DEBT-001 โ€” Auth Service: JWT secret rotation not implemented

  • Impact: Security risk; requires manual key rotation with downtime
  • Owner: platform-team
  • Priority: High
  • Target: Q3 2026
  • Estimate: 1 sprint
  • Context: Originally planned for v1, deferred to ship on time (ADR-0007)

DEBT-007 โ€” Orders API: N+1 query in order list endpoint

  • Impact: Response time degrades linearly with order items count; P99 at 800ms
  • Owner: payments-team
  • Priority: Medium
  • Target: Q3 2026
  • Estimate: 2 days
  • Context: Quick fix: eager-load items with include in Prisma query

DEBT-013 โ€” Frontend: No error boundaries; uncaught exceptions crash whole app

  • Impact: Poor user experience; affects ~2% of sessions (Sentry data)
  • Owner: frontend-team
  • Priority: Medium
  • Target: Q4 2026
  • Estimate: 1 week (all major routes)

DEBT-019 โ€” Infrastructure: Manual RDS snapshot process

  • Impact: Backup not verified; last restore test was 8 months ago
  • Owner: platform-team
  • Priority: High
  • Target: Q3 2026
  • Estimate: 3 days
  • Context: AWS Backup should automate + test restore quarterly

Resolved This Quarter

  • DEBT-004: Replaced custom auth library with Clerk SDK โœ… (2026-06-12)
  • DEBT-009: Migrated from CJS to ESM throughout โœ… (2026-06-28)

---

Fitness Functions: Architecture Validation as Tests

Fitness functions are automated checks that verify your architecture stays healthy over time. They run in CI and fail the build when architectural rules are violated โ€” before production.

Dependency Direction Checks

// tests/architecture/dependency-rules.test.ts
import { describe, it, expect } from 'vitest';
import { findImports } from './helpers/find-imports';

describe('Architecture: dependency rules', () => {
  it('domain layer does not import from infrastructure layer', async () => {
    const infraImports = await findImports(
      'src/domain/**/*.ts',
      ['src/infrastructure', 'src/adapters', 'node_modules/prisma'],
    );

    expect(infraImports).toEqual([]);
  });

  it('services layer does not import from routes layer', async () => {
    const violations = await findImports(
      'src/services/**/*.ts',
      ['src/routes', 'src/controllers'],
    );

    expect(violations).toEqual([]);
  });

  it('shared utilities have no app-specific imports', async () => {
    const violations = await findImports(
      'libs/utils/**/*.ts',
      ['src/', 'apps/'],
    );

    expect(violations).toEqual([]);
  });
});
// tests/architecture/helpers/find-imports.ts
import { glob } from 'glob';
import * as fs from 'fs';
import * as path from 'path';

export async function findImports(
  sourcePattern: string,
  forbiddenPrefixes: string[],
): Promise<Array<{ file: string; import: string }>> {
  const files = await glob(sourcePattern);
  const violations: Array<{ file: string; import: string }> = [];

  for (const file of files) {
    const content = fs.readFileSync(file, 'utf-8');
    const importRegex = /(?:import|require)\s*\(?['"](.*?)['"]\)?/g;

    let match;
    while ((match = importRegex.exec(content)) !== null) {
      const importPath = match[1];
      const resolvedPath = path.resolve(path.dirname(file), importPath);

      for (const forbidden of forbiddenPrefixes) {
        if (resolvedPath.includes(forbidden) || importPath.includes(forbidden)) {
          violations.push({ file, import: importPath });
        }
      }
    }
  }

  return violations;
}

Performance Fitness Functions

// tests/architecture/performance-budget.test.ts
import { describe, it, expect } from 'vitest';
import { execSync } from 'child_process';
import * as fs from 'fs';

describe('Architecture: performance budgets', () => {
  it('Next.js bundle size stays within budget', () => {
    // Run build and check bundle sizes
    const buildManifest = JSON.parse(
      fs.readFileSync('.next/build-manifest.json', 'utf-8'),
    );

    // Main app bundle must be <200KB gzipped
    const mainBundles = Object.entries(buildManifest.pages)
      .filter(([page]) => page === '/')
      .flatMap(([, files]) => files as string[]);

    // This is a simplified check โ€” use bundle-analyzer for real measurement
    expect(mainBundles.length).toBeGreaterThan(0);
  });

  it('database schema has indexes on all FK columns', async () => {
    const { db } = await import('@/db');

    // Query PostgreSQL information schema
    const unindexedFKs = await db.query<{ table: string; column: string }>(`
      SELECT
        tc.table_name AS table,
        kcu.column_name AS column
      FROM information_schema.table_constraints tc
      JOIN information_schema.key_column_usage kcu
        ON tc.constraint_name = kcu.constraint_name
      WHERE tc.constraint_type = 'FOREIGN KEY'
        AND NOT EXISTS (
          SELECT 1 FROM pg_indexes
          WHERE tablename = tc.table_name
            AND indexdef LIKE '%' || kcu.column_name || '%'
        )
    `);

    expect(unindexedFKs.rows).toEqual([]);
  });

  it('no API endpoint exceeds 2000ms P95 in load test', () => {
    // Run k6 smoke test and check results
    const result = execSync('k6 run --summary-export=k6-summary.json tests/load/smoke.js', {
      stdio: 'pipe',
    });

    const summary = JSON.parse(fs.readFileSync('k6-summary.json', 'utf-8'));
    const p95 = summary.metrics?.http_req_duration?.values?.['p(95)'];

    expect(p95).toBeLessThan(2000);
  });
});

Security Fitness Functions

# .github/workflows/security-fitness.yml
name: Security Fitness Functions
on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # No high/critical vulnerabilities in dependencies
      - name: Dependency audit
        run: npm audit --audit-level=high

      # No secrets committed to repo
      - name: Secret scan
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}

      # No OWASP Top 10 in code
      - name: SAST scan
        uses: SonarSource/sonarcloud-github-action@master
        with:
          projectBaseDir: .
          args: >
            -Dsonar.qualitygate.wait=true

      # License compliance
      - name: License check
        run: npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC'

Architecture Review Cadence

Review TypeFrequencyParticipantsOutput
ADR ReviewPer-feature (async)Author + 1โ€“2 reviewersAccepted/revised ADR
Tech Debt ReviewQuarterlyTeam leadsPrioritized debt backlog
Architecture FitnessEvery CI runAutomatedPass/fail metrics
Design ReviewFor significant changesFull teamDesign doc + ADR
Annual Architecture ReviewYearlyEngineering + CTOStrategic roadmap

arc42: Lightweight Architecture Documentation

arc42 provides a 12-section template for documenting the architecture of existing systems. Unlike ADRs (which capture decisions), arc42 captures the current state.

# arc42 Architecture Documentation: MyApp

## 1. Introduction and Goals
- Business context and key stakeholders
- Quality goals: reliability >99.9%, P95 <500ms, GDPR compliant

2. Constraints

  • Must run on AWS (existing contract)
  • Engineering team: 8 developers (no dedicated ops)
  • Regulatory: SOC 2 Type II compliance required

3. System Scope and Context

  • External systems: Stripe, SendGrid, Auth0, PagerDuty
  • Users: SaaS customers, admins, integration partners

4. Solution Strategy

  • Monolith-first, microservices only when team/scale demands
  • PostgreSQL as primary store, Redis for cache/sessions
  • Serverless for async processing (AWS Lambda)

5. Building Block View

  • See: docs/architecture/components.png
  • API layer (Fastify) โ†’ Service layer โ†’ Repository layer โ†’ PostgreSQL

6. Runtime View

  • Order creation flow (sequence diagram)
  • Authentication flow (sequence diagram)

7. Deployment View

  • AWS ECS Fargate for API, RDS PostgreSQL, ElastiCache Redis
  • GitHub Actions CI/CD โ†’ ECR โ†’ ECS Blue/Green

8. Cross-Cutting Concepts

  • Error handling: all errors wrapped in AppError with code/message
  • Logging: structured JSON via pino, correlated with trace IDs
  • Auth: JWT (access 15min) + refresh tokens (7 days) via HTTP-only cookie

9. Architecture Decisions

  • See: docs/decisions/ (ADR index)

10. Quality Requirements

  • Reliability: 99.9% uptime SLO
  • Performance: P95 API <500ms, P99 <2s
  • Security: OWASP Top 10 addressed, SOC 2 Type II

11. Risks and Technical Debt

  • See: docs/technical-debt-register.md

12. Glossary

  • Tenant: a paying customer organization
  • ADR: Architecture Decision Record

---

## Working With Viprasol

We run architecture reviews for engineering teams โ€” establishing ADR processes, auditing technical debt, and setting up fitness functions that enforce architectural health in CI.

**What we deliver:**
- ADR process setup + initial ADRs for existing key decisions
- Technical debt audit and prioritized register
- Fitness functions for dependency rules, bundle budgets, and security
- arc42 architecture documentation for your system
- Quarterly architecture review process design

โ†’ [Discuss your architecture needs](/contact)
โ†’ [Software development services](/services/web-development/)

---
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.