What are Architecture Fitness Functions?
Architecture Fitness Functions, introduced in "Building Evolutionary Architectures" by Neal Ford, Rebecca Parsons, and Patrick Kua, are automated checks that validate whether an architecture meets its intended goals. They are to architecture what unit tests are to code — objective, automated measures that prevent architecture from degrading over time.
Types of Fitness Functions
- Atomic: Tests that run against a single architectural characteristic (e.g., no circular dependencies)
- Holistic: Tests that validate system-wide properties (e.g., end-to-end latency under load)
- Triggered: Run on specific events (code commit, deployment) — typically in CI/CD pipelines
- Continuous: Run constantly (production monitoring, real-time metrics)
Dependency Governance
// Fitness function: Enforce module dependency rules
import * as ts from "typescript";
import * as path from "path";
interface DependencyRule {
module: string;
allowedDependencies: string[];
forbiddenDependencies: string[];
}
const dependencyRules: DependencyRule[] = [
{
module: "domain",
allowedDependencies: [], // Domain depends on NOTHING
forbiddenDependencies: ["infrastructure", "adapters", "express", "pg", "mongodb"],
},
{
module: "use-cases",
allowedDependencies: ["domain"],
forbiddenDependencies: ["infrastructure", "adapters", "express"],
},
{
module: "adapters",
allowedDependencies: ["domain", "use-cases"],
forbiddenDependencies: [],
},
];
// Test: No circular dependencies between modules
function checkCircularDependencies(projectPath: string): string[] {
const violations: string[] = [];
const moduleGraph = buildModuleGraph(projectPath);
for (const [module, deps] of moduleGraph.entries()) {
for (const dep of deps) {
if (moduleGraph.get(dep)?.has(module)) {
violations.push(`Circular dependency: ${module} <-> ${dep}`);
}
}
}
return violations;
}
// Test: Layer dependency rules
function checkLayerDependencies(projectPath: string): string[] {
const violations: string[] = [];
for (const rule of dependencyRules) {
const moduleFiles = getFilesInModule(projectPath, rule.module);
for (const file of moduleFiles) {
const imports = getImports(file);
for (const imp of imports) {
for (const forbidden of rule.forbiddenDependencies) {
if (imp.includes(forbidden)) {
violations.push(
`${file}: Module "${rule.module}" must not import from "${forbidden}"`
);
}
}
}
}
}
return violations;
}
// Run as part of CI/CD
describe("Architecture Fitness Functions", () => {
it("should have no circular dependencies", () => {
const violations = checkCircularDependencies("./src");
expect(violations).toEqual([]);
});
it("should respect layer dependency rules", () => {
const violations = checkLayerDependencies("./src");
expect(violations).toEqual([]);
});
});
Code Quality Metrics
// Fitness function: Code complexity limits
interface ComplexityMetrics {
file: string;
cyclomaticComplexity: number;
linesOfCode: number;
dependencies: number;
exportedSymbols: number;
}
function checkComplexityLimits(metrics: ComplexityMetrics[]): string[] {
const violations: string[] = [];
const limits = {
maxCyclomaticComplexity: 15,
maxLinesPerFile: 300,
maxDependenciesPerFile: 10,
maxExportsPerFile: 5,
};
for (const file of metrics) {
if (file.cyclomaticComplexity > limits.maxCyclomaticComplexity) {
violations.push(
`${file.file}: Cyclomatic complexity ${file.cyclomaticComplexity} exceeds limit ${limits.maxCyclomaticComplexity}`
);
}
if (file.linesOfCode > limits.maxLinesPerFile) {
violations.push(
`${file.file}: ${file.linesOfCode} lines exceeds limit ${limits.maxLinesPerFile}`
);
}
if (file.dependencies > limits.maxDependenciesPerFile) {
violations.push(
`${file.file}: ${file.dependencies} imports exceeds limit ${limits.maxDependenciesPerFile}`
);
}
}
return violations;
}
// Fitness function: API contract stability
interface APIEndpoint {
method: string;
path: string;
requestSchema: string; // JSON Schema hash
responseSchema: string;
}
function checkAPIContractStability(
currentAPIs: APIEndpoint[],
previousAPIs: APIEndpoint[]
): string[] {
const violations: string[] = [];
for (const prev of previousAPIs) {
const current = currentAPIs.find(
a => a.method === prev.method && a.path === prev.path
);
if (!current) {
violations.push(`Removed API endpoint: ${prev.method} ${prev.path}`);
} else if (current.requestSchema !== prev.requestSchema) {
violations.push(
`Breaking change in request schema: ${prev.method} ${prev.path}`
);
}
}
return violations;
}
Performance Fitness Functions
// Fitness function: API response time budgets
interface PerformanceBudget {
endpoint: string;
p50MaxMs: number;
p95MaxMs: number;
p99MaxMs: number;
}
const performanceBudgets: PerformanceBudget[] = [
{ endpoint: "/api/products", p50MaxMs: 50, p95MaxMs: 200, p99MaxMs: 500 },
{ endpoint: "/api/orders", p50MaxMs: 100, p95MaxMs: 300, p99MaxMs: 800 },
{ endpoint: "/api/search", p50MaxMs: 150, p95MaxMs: 500, p99MaxMs: 1000 },
];
async function checkPerformanceBudgets(
metricsService: MetricsService
): Promise {
const violations: string[] = [];
for (const budget of performanceBudgets) {
const metrics = await metricsService.getLatencyPercentiles(
budget.endpoint,
{ period: "1h" }
);
if (metrics.p50 > budget.p50MaxMs) {
violations.push(
`${budget.endpoint} p50 latency ${metrics.p50}ms exceeds budget ${budget.p50MaxMs}ms`
);
}
if (metrics.p95 > budget.p95MaxMs) {
violations.push(
`${budget.endpoint} p95 latency ${metrics.p95}ms exceeds budget ${budget.p95MaxMs}ms`
);
}
if (metrics.p99 > budget.p99MaxMs) {
violations.push(
`${budget.endpoint} p99 latency ${metrics.p99}ms exceeds budget ${budget.p99MaxMs}ms`
);
}
}
return violations;
}
// Fitness function: Bundle size budget for frontend
interface BundleBudget {
chunk: string;
maxSizeKb: number;
}
const bundleBudgets: BundleBudget[] = [
{ chunk: "main", maxSizeKb: 200 },
{ chunk: "vendor", maxSizeKb: 300 },
{ chunk: "total", maxSizeKb: 500 },
];
function checkBundleSize(buildOutput: BuildManifest): string[] {
const violations: string[] = [];
for (const budget of bundleBudgets) {
const actualSize = buildOutput.chunks[budget.chunk]?.sizeKb || 0;
if (actualSize > budget.maxSizeKb) {
violations.push(
`Bundle "${budget.chunk}" is ${actualSize}KB (limit: ${budget.maxSizeKb}KB)`
);
}
}
return violations;
}
CI/CD Integration
// fitness-functions.test.ts — Run in CI/CD pipeline
describe("Architecture Fitness Functions", () => {
describe("Structural", () => {
it("domain layer has no external dependencies", () => {
const violations = checkLayerDependencies("./src");
expect(violations).toHaveLength(0);
});
it("no circular dependencies between modules", () => {
const violations = checkCircularDependencies("./src");
expect(violations).toHaveLength(0);
});
it("module boundaries are respected", () => {
const violations = checkModuleBoundaries("./src");
expect(violations).toHaveLength(0);
});
});
describe("Code Quality", () => {
it("files stay within complexity limits", () => {
const metrics = analyzeComplexity("./src");
const violations = checkComplexityLimits(metrics);
expect(violations).toHaveLength(0);
});
it("test coverage meets minimum threshold", () => {
const coverage = getCoverageReport();
expect(coverage.overall).toBeGreaterThan(80);
expect(coverage.domain).toBeGreaterThan(95);
});
});
describe("API Contracts", () => {
it("no breaking changes in public APIs", () => {
const current = parseOpenAPISpec("./openapi.yaml");
const previous = parseOpenAPISpec("./openapi.previous.yaml");
const violations = checkAPIContractStability(current, previous);
expect(violations).toHaveLength(0);
});
});
describe("Performance", () => {
it("bundle size within budget", () => {
const manifest = readBuildManifest("./dist");
const violations = checkBundleSize(manifest);
expect(violations).toHaveLength(0);
});
});
});
Fitness Function Best Practices
- Automate everything: Fitness functions that require manual checks will be ignored — automate them in CI/CD
- Fail the build: If a fitness function fails, the build should fail — treat architecture violations like test failures
- Start small: Begin with the most critical architectural constraints and add more over time
- Document the why: Each fitness function should have a clear explanation of what architectural goal it protects
- Review thresholds: Regularly review and adjust thresholds as the system evolves