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.
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
| Option | Pros | Cons |
|---|---|---|
| Separate databases | Perfect isolation, easy backup per tenant | Ops complexity scales with tenant count |
| Separate schemas | Good isolation, per-tenant migrations possible | Migration fan-out problem at 500 tenants |
| Shared schema + RLS | Simple ops, single migration path | RLS 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 LOCALmust 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 LOCALbefore 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
includein 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 Type | Frequency | Participants | Output |
|---|---|---|---|
| ADR Review | Per-feature (async) | Author + 1โ2 reviewers | Accepted/revised ADR |
| Tech Debt Review | Quarterly | Team leads | Prioritized debt backlog |
| Architecture Fitness | Every CI run | Automated | Pass/fail metrics |
| Design Review | For significant changes | Full team | Design doc + ADR |
| Annual Architecture Review | Yearly | Engineering + CTO | Strategic 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/)
---
About the Author
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.
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
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.