Defining Done for Architecture Work: How to Measure What's Hard to Measure
Defining Done for Architecture Work: How to Measure What's Hard to Measure
The Perpetual Work-in-Progress Problem
Architecture work has a visibility problem. Features ship with fanfare—"Users can now export to PDF!" Architecture improvements ship silently—"We reduced coupling between the auth and billing modules." Stakeholders understand the first; they struggle to value the second. Without clear metrics and milestones, architecture work becomes a perpetual expense with no apparent return.
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE ARCHITECTURE VISIBILITY GAP │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ FEATURE WORK ARCHITECTURE WORK │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ │ │ │ │
│ │ ✓ Clear deliverable │ │ ? Ongoing investment │ │
│ │ ✓ Demo-able │ │ ? Hard to demo │ │
│ │ ✓ User-facing │ │ ? Developer-facing │ │
│ │ ✓ Obvious when done │ │ ? Never "done" │ │
│ │ ✓ Easy to prioritize │ │ ? Competes with features│ │
│ │ │ │ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ STAKEHOLDER PERCEPTION │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ "We shipped 12 features this quarter" │ │
│ │ vs │ │
│ │ "We improved the architecture" │ │
│ │ │ │
│ │ One is concrete. One sounds like an excuse. │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ THE SOLUTION: Make architecture measurable │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Leading Indicators → Lagging Indicators │ │
│ │ (Measure during work) (Measure outcomes) │ │
│ │ │ │
│ │ - Module coupling score - Feature delivery time │ │
│ │ - Build time - Bug rate in changed areas │ │
│ │ - Test coverage of seams - Onboarding time for new devs │ │
│ │ - Dependency graph depth - Time to add new integration │ │
│ │ - Code ownership clarity - Incident frequency │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Framework: The Architecture Health Dashboard
Architecture health isn't a single metric—it's a constellation of indicators that together paint a picture of system quality.
// lib/architecture-metrics/health-dashboard.ts
// Automated architecture health measurement
interface ArchitectureHealthMetrics {
// Structural Health
coupling: {
score: number; // 0-100, lower is better
moduleCount: number;
averageDependencies: number;
cyclicDependencies: number;
highestCoupledModules: Array<{ module: string; dependencyCount: number }>;
};
// Build & Deploy Health
build: {
averageBuildTime: number; // seconds
buildTimeP95: number;
cacheHitRate: number; // percentage
incrementalBuildTime: number;
};
// Test Health
testing: {
coverage: number; // percentage
integrationTestCount: number;
averageTestDuration: number;
flakyTestCount: number;
testToCodeRatio: number;
};
// Dependency Health
dependencies: {
directCount: number;
transitiveCount: number;
outdatedCount: number;
vulnerableCount: number;
averageAge: number; // days since last update
};
// Codebase Health
codebase: {
totalLines: number;
duplicationPercentage: number;
averageFileSize: number;
largestFiles: Array<{ path: string; lines: number }>;
deadCodeEstimate: number;
};
// Team Health (code-related)
team: {
busFactorScore: number; // 1-10, higher is better
averageOwnership: number; // files per primary owner
orphanedFiles: number; // files with no clear owner
recentContributors: number;
};
}
// Health score calculation
interface HealthScore {
overall: number; // 0-100
trend: 'improving' | 'stable' | 'declining';
breakdown: {
category: string;
score: number;
weight: number;
trend: 'improving' | 'stable' | 'declining';
}[];
alerts: {
severity: 'critical' | 'warning' | 'info';
category: string;
message: string;
metric: string;
value: number;
threshold: number;
}[];
}
function calculateHealthScore(
current: ArchitectureHealthMetrics,
previous: ArchitectureHealthMetrics
): HealthScore {
const breakdown: HealthScore['breakdown'] = [];
const alerts: HealthScore['alerts'] = [];
// Coupling score (weight: 25%)
const couplingScore = Math.max(0, 100 - current.coupling.score);
const couplingTrend = getTrend(previous.coupling.score, current.coupling.score, true);
breakdown.push({ category: 'Coupling', score: couplingScore, weight: 0.25, trend: couplingTrend });
if (current.coupling.cyclicDependencies > 0) {
alerts.push({
severity: 'critical',
category: 'Coupling',
message: `${current.coupling.cyclicDependencies} cyclic dependencies detected`,
metric: 'cyclicDependencies',
value: current.coupling.cyclicDependencies,
threshold: 0,
});
}
// Build health score (weight: 20%)
const buildScore = calculateBuildScore(current.build);
const buildTrend = getTrend(
previous.build.averageBuildTime,
current.build.averageBuildTime,
true
);
breakdown.push({ category: 'Build', score: buildScore, weight: 0.20, trend: buildTrend });
if (current.build.averageBuildTime > 300) { // 5 minutes
alerts.push({
severity: 'warning',
category: 'Build',
message: `Average build time exceeds 5 minutes`,
metric: 'averageBuildTime',
value: current.build.averageBuildTime,
threshold: 300,
});
}
// Test health score (weight: 20%)
const testScore = calculateTestScore(current.testing);
const testTrend = getTrend(previous.testing.coverage, current.testing.coverage, false);
breakdown.push({ category: 'Testing', score: testScore, weight: 0.20, trend: testTrend });
if (current.testing.flakyTestCount > 5) {
alerts.push({
severity: 'warning',
category: 'Testing',
message: `${current.testing.flakyTestCount} flaky tests detected`,
metric: 'flakyTestCount',
value: current.testing.flakyTestCount,
threshold: 5,
});
}
// Dependency health score (weight: 15%)
const depScore = calculateDependencyScore(current.dependencies);
const depTrend = getTrend(
previous.dependencies.outdatedCount,
current.dependencies.outdatedCount,
true
);
breakdown.push({ category: 'Dependencies', score: depScore, weight: 0.15, trend: depTrend });
if (current.dependencies.vulnerableCount > 0) {
alerts.push({
severity: 'critical',
category: 'Dependencies',
message: `${current.dependencies.vulnerableCount} vulnerable dependencies`,
metric: 'vulnerableCount',
value: current.dependencies.vulnerableCount,
threshold: 0,
});
}
// Codebase health score (weight: 10%)
const codeScore = calculateCodebaseScore(current.codebase);
const codeTrend = getTrend(
previous.codebase.duplicationPercentage,
current.codebase.duplicationPercentage,
true
);
breakdown.push({ category: 'Codebase', score: codeScore, weight: 0.10, trend: codeTrend });
// Team health score (weight: 10%)
const teamScore = current.team.busFactorScore * 10;
const teamTrend = getTrend(
previous.team.busFactorScore,
current.team.busFactorScore,
false
);
breakdown.push({ category: 'Team', score: teamScore, weight: 0.10, trend: teamTrend });
if (current.team.busFactorScore < 3) {
alerts.push({
severity: 'warning',
category: 'Team',
message: 'Low bus factor - knowledge concentrated in few people',
metric: 'busFactorScore',
value: current.team.busFactorScore,
threshold: 3,
});
}
// Calculate overall score
const overall = breakdown.reduce((sum, b) => sum + b.score * b.weight, 0);
// Determine overall trend
const improvingCount = breakdown.filter(b => b.trend === 'improving').length;
const decliningCount = breakdown.filter(b => b.trend === 'declining').length;
const trend = improvingCount > decliningCount ? 'improving' :
decliningCount > improvingCount ? 'declining' : 'stable';
return { overall, trend, breakdown, alerts };
}
function getTrend(
previous: number,
current: number,
lowerIsBetter: boolean
): 'improving' | 'stable' | 'declining' {
const threshold = 0.05; // 5% change threshold
const change = (current - previous) / (previous || 1);
if (Math.abs(change) < threshold) return 'stable';
if (lowerIsBetter) {
return change < 0 ? 'improving' : 'declining';
} else {
return change > 0 ? 'improving' : 'declining';
}
}
Measuring Coupling: The Primary Architecture Metric
Coupling is the most important architecture metric. High coupling means changes ripple through the codebase; low coupling means changes are localized.
// lib/architecture-metrics/coupling.ts
// Measure module coupling in a codebase
import * as fs from 'fs';
import * as path from 'path';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import { glob } from 'glob';
interface ModuleDependency {
source: string;
target: string;
type: 'import' | 'export' | 'type';
importCount: number;
}
interface CouplingAnalysis {
modules: Map<string, ModuleInfo>;
dependencies: ModuleDependency[];
metrics: {
totalModules: number;
totalDependencies: number;
averageAfferentCoupling: number; // incoming dependencies
averageEfferentCoupling: number; // outgoing dependencies
instabilityDistribution: number[];
cyclicDependencies: string[][];
couplingScore: number; // 0-100
};
}
interface ModuleInfo {
name: string;
files: string[];
afferentCoupling: number; // modules that depend on this
efferentCoupling: number; // modules this depends on
instability: number; // Ce / (Ca + Ce), 0 = stable, 1 = unstable
abstractness: number; // ratio of interfaces/types to implementations
distanceFromMainSequence: number; // |A + I - 1|
}
// Define module boundaries
function inferModuleFromPath(filePath: string): string {
// Customize based on your project structure
const parts = filePath.split('/');
// packages/*/src/** -> @scope/package-name
if (parts[0] === 'packages' && parts.length > 1) {
return `@company/${parts[1]}`;
}
// apps/*/src/** -> app-name
if (parts[0] === 'apps' && parts.length > 1) {
return `app:${parts[1]}`;
}
// src/modules/** -> module-name
if (parts[0] === 'src' && parts[1] === 'modules' && parts.length > 2) {
return `module:${parts[2]}`;
}
// src/features/** -> feature-name
if (parts[0] === 'src' && parts[1] === 'features' && parts.length > 2) {
return `feature:${parts[2]}`;
}
// Default: top-level directory
return parts[0] || 'root';
}
async function analyzeCoupling(rootDir: string): Promise<CouplingAnalysis> {
const files = await glob('**/*.{ts,tsx,js,jsx}', {
cwd: rootDir,
ignore: ['node_modules/**', 'dist/**', 'build/**', '*.test.*', '*.spec.*'],
});
const modules = new Map<string, ModuleInfo>();
const dependencies: ModuleDependency[] = [];
const dependencyMap = new Map<string, Set<string>>(); // source -> targets
// First pass: collect all imports
for (const file of files) {
const filePath = path.join(rootDir, file);
const sourceModule = inferModuleFromPath(file);
// Ensure module exists
if (!modules.has(sourceModule)) {
modules.set(sourceModule, {
name: sourceModule,
files: [],
afferentCoupling: 0,
efferentCoupling: 0,
instability: 0,
abstractness: 0,
distanceFromMainSequence: 0,
});
}
modules.get(sourceModule)!.files.push(file);
// Parse and find imports
const imports = extractImports(filePath);
for (const imp of imports) {
const targetModule = resolveImportToModule(imp, file, rootDir);
if (!targetModule || targetModule === sourceModule) continue;
// Ensure target module exists
if (!modules.has(targetModule)) {
modules.set(targetModule, {
name: targetModule,
files: [],
afferentCoupling: 0,
efferentCoupling: 0,
instability: 0,
abstractness: 0,
distanceFromMainSequence: 0,
});
}
// Record dependency
if (!dependencyMap.has(sourceModule)) {
dependencyMap.set(sourceModule, new Set());
}
dependencyMap.get(sourceModule)!.add(targetModule);
dependencies.push({
source: sourceModule,
target: targetModule,
type: imp.isTypeOnly ? 'type' : 'import',
importCount: 1,
});
}
}
// Calculate coupling metrics
for (const [name, info] of modules) {
// Efferent coupling: modules this depends on
info.efferentCoupling = dependencyMap.get(name)?.size || 0;
// Afferent coupling: modules that depend on this
info.afferentCoupling = Array.from(dependencyMap.values())
.filter(deps => deps.has(name))
.length;
// Instability: I = Ce / (Ca + Ce)
const total = info.afferentCoupling + info.efferentCoupling;
info.instability = total > 0 ? info.efferentCoupling / total : 0;
}
// Detect cyclic dependencies
const cycles = detectCycles(dependencyMap);
// Calculate overall coupling score
const avgEfferent = Array.from(modules.values())
.reduce((sum, m) => sum + m.efferentCoupling, 0) / modules.size;
const avgAfferent = Array.from(modules.values())
.reduce((sum, m) => sum + m.afferentCoupling, 0) / modules.size;
// Score: lower coupling = higher score
// Penalize based on average dependencies and cycles
let couplingScore = 100;
couplingScore -= Math.min(30, avgEfferent * 3); // Up to 30 points for avg dependencies
couplingScore -= Math.min(30, cycles.length * 10); // Up to 30 points for cycles
couplingScore -= Math.min(20, (dependencies.length / modules.size) * 2); // Density penalty
couplingScore = Math.max(0, couplingScore);
return {
modules,
dependencies,
metrics: {
totalModules: modules.size,
totalDependencies: dependencies.length,
averageAfferentCoupling: avgAfferent,
averageEfferentCoupling: avgEfferent,
instabilityDistribution: Array.from(modules.values()).map(m => m.instability),
cyclicDependencies: cycles,
couplingScore,
},
};
}
function detectCycles(dependencyMap: Map<string, Set<string>>): string[][] {
const cycles: string[][] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
const path: string[] = [];
function dfs(node: string): void {
visited.add(node);
recursionStack.add(node);
path.push(node);
const dependencies = dependencyMap.get(node) || new Set();
for (const dep of dependencies) {
if (!visited.has(dep)) {
dfs(dep);
} else if (recursionStack.has(dep)) {
// Found cycle
const cycleStart = path.indexOf(dep);
cycles.push(path.slice(cycleStart));
}
}
path.pop();
recursionStack.delete(node);
}
for (const node of dependencyMap.keys()) {
if (!visited.has(node)) {
dfs(node);
}
}
return cycles;
}
// Generate coupling report
function generateCouplingReport(analysis: CouplingAnalysis): string {
let report = '# Module Coupling Analysis\n\n';
report += '## Summary\n\n';
report += `- **Total Modules:** ${analysis.metrics.totalModules}\n`;
report += `- **Total Dependencies:** ${analysis.metrics.totalDependencies}\n`;
report += `- **Average Outgoing Dependencies:** ${analysis.metrics.averageEfferentCoupling.toFixed(2)}\n`;
report += `- **Average Incoming Dependencies:** ${analysis.metrics.averageAfferentCoupling.toFixed(2)}\n`;
report += `- **Cyclic Dependencies:** ${analysis.metrics.cyclicDependencies.length}\n`;
report += `- **Coupling Score:** ${analysis.metrics.couplingScore.toFixed(0)}/100\n\n`;
if (analysis.metrics.cyclicDependencies.length > 0) {
report += '## ⚠️ Cyclic Dependencies\n\n';
for (const cycle of analysis.metrics.cyclicDependencies) {
report += `- ${cycle.join(' → ')} → ${cycle[0]}\n`;
}
report += '\n';
}
report += '## Modules by Instability\n\n';
report += '| Module | Outgoing | Incoming | Instability |\n';
report += '|--------|----------|----------|-------------|\n';
const sortedModules = Array.from(analysis.modules.values())
.sort((a, b) => b.instability - a.instability);
for (const module of sortedModules) {
const stability = module.instability < 0.3 ? '🟢' :
module.instability < 0.7 ? '🟡' : '🔴';
report += `| ${module.name} | ${module.efferentCoupling} | ${module.afferentCoupling} | ${stability} ${module.instability.toFixed(2)} |\n`;
}
return report;
}
Tracking Architecture Initiatives
Architecture work needs the same project management rigor as feature work—milestones, acceptance criteria, and progress tracking.
// lib/architecture-initiatives/tracker.ts
// Track architecture initiatives with measurable milestones
interface ArchitectureInitiative {
id: string;
title: string;
description: string;
owner: string;
status: 'proposed' | 'in-progress' | 'completed' | 'abandoned';
priority: 'critical' | 'high' | 'medium' | 'low';
// Problem statement
problem: {
description: string;
impactAreas: string[];
currentMetrics: Record<string, number>;
};
// Success criteria (measurable!)
successCriteria: Array<{
metric: string;
baseline: number;
target: number;
current: number;
unit: string;
}>;
// Milestones with concrete deliverables
milestones: Array<{
id: string;
title: string;
description: string;
deliverables: string[];
targetDate: Date;
completedDate?: Date;
status: 'pending' | 'in-progress' | 'completed' | 'blocked';
}>;
// Time tracking
timeline: {
proposedDate: Date;
approvedDate?: Date;
startedDate?: Date;
targetCompletionDate?: Date;
actualCompletionDate?: Date;
};
// Investment tracking
investment: {
estimatedEffort: string; // "2 sprints", "1 quarter", etc.
actualEffort?: string;
teamMembers: string[];
sprintAllocation: number; // percentage of sprint capacity
};
}
// Example initiative definition
const exampleInitiative: ArchitectureInitiative = {
id: 'arch-2024-001',
title: 'Decouple Auth from Billing Module',
description: `
The auth and billing modules are tightly coupled, requiring changes
to billing whenever auth policies change. This initiative introduces
an event-driven boundary between them.
`,
owner: 'jane.architect',
status: 'in-progress',
priority: 'high',
problem: {
description: `
Auth changes require billing changes 80% of the time.
Average PR touches 12 files across both modules.
New auth features take 3x longer due to billing coordination.
`,
impactAreas: ['auth', 'billing', 'user-management'],
currentMetrics: {
'coupled-changes-percentage': 80,
'average-files-per-pr': 12,
'auth-feature-time-days': 15,
},
},
successCriteria: [
{
metric: 'Coupled changes percentage',
baseline: 80,
target: 10,
current: 45,
unit: '%',
},
{
metric: 'Average files per cross-module PR',
baseline: 12,
target: 3,
current: 7,
unit: 'files',
},
{
metric: 'Auth feature delivery time',
baseline: 15,
target: 5,
current: 10,
unit: 'days',
},
{
metric: 'Module coupling score',
baseline: 85,
target: 30,
current: 55,
unit: 'score',
},
],
milestones: [
{
id: 'ms-1',
title: 'Define event contract',
description: 'Design the event schema for auth-billing communication',
deliverables: [
'Event schema documentation',
'TypeScript types for events',
'ADR for event-driven approach',
],
targetDate: new Date('2024-02-15'),
completedDate: new Date('2024-02-14'),
status: 'completed',
},
{
id: 'ms-2',
title: 'Implement event bus',
description: 'Set up event infrastructure',
deliverables: [
'Event bus implementation',
'Publisher/subscriber patterns',
'Dead letter queue handling',
'Monitoring dashboard',
],
targetDate: new Date('2024-03-01'),
completedDate: new Date('2024-03-05'),
status: 'completed',
},
{
id: 'ms-3',
title: 'Migrate auth events',
description: 'Auth module publishes events instead of direct calls',
deliverables: [
'Auth publishes UserCreated, UserUpdated, RoleChanged events',
'Billing subscribes to relevant events',
'Feature flag for gradual rollout',
'Monitoring for event flow',
],
targetDate: new Date('2024-03-15'),
status: 'in-progress',
},
{
id: 'ms-4',
title: 'Remove direct dependencies',
description: 'Delete direct imports from auth to billing',
deliverables: [
'No direct imports between modules',
'Shared types moved to contracts package',
'Documentation updated',
'Coupling metrics meet target',
],
targetDate: new Date('2024-04-01'),
status: 'pending',
},
],
timeline: {
proposedDate: new Date('2024-01-15'),
approvedDate: new Date('2024-01-22'),
startedDate: new Date('2024-02-01'),
targetCompletionDate: new Date('2024-04-01'),
},
investment: {
estimatedEffort: '4 sprints',
teamMembers: ['jane.architect', 'bob.backend', 'alice.backend'],
sprintAllocation: 30, // 30% of sprint capacity
},
};
// Progress calculation
function calculateInitiativeProgress(initiative: ArchitectureInitiative): {
milestonesComplete: number;
milestonesTotal: number;
criteriaProgress: number;
overallProgress: number;
onTrack: boolean;
riskLevel: 'low' | 'medium' | 'high';
} {
const milestonesComplete = initiative.milestones.filter(
m => m.status === 'completed'
).length;
const milestonesTotal = initiative.milestones.length;
// Calculate criteria progress (average of individual progress)
const criteriaProgress = initiative.successCriteria.reduce((sum, c) => {
const range = c.baseline - c.target;
const progress = (c.baseline - c.current) / range;
return sum + Math.max(0, Math.min(1, progress));
}, 0) / initiative.successCriteria.length;
// Overall progress: 50% milestones, 50% criteria
const overallProgress = (
(milestonesComplete / milestonesTotal) * 0.5 +
criteriaProgress * 0.5
);
// Check if on track
const now = new Date();
const targetDate = initiative.timeline.targetCompletionDate;
const startDate = initiative.timeline.startedDate || now;
const totalDuration = targetDate ? targetDate.getTime() - startDate.getTime() : 0;
const elapsed = now.getTime() - startDate.getTime();
const expectedProgress = totalDuration > 0 ? elapsed / totalDuration : 0;
const onTrack = overallProgress >= expectedProgress * 0.9; // Within 10%
// Risk assessment
let riskLevel: 'low' | 'medium' | 'high' = 'low';
if (!onTrack) riskLevel = 'medium';
if (initiative.milestones.some(m => m.status === 'blocked')) riskLevel = 'high';
if (overallProgress < expectedProgress * 0.7) riskLevel = 'high';
return {
milestonesComplete,
milestonesTotal,
criteriaProgress: criteriaProgress * 100,
overallProgress: overallProgress * 100,
onTrack,
riskLevel,
};
}
Communicating to Stakeholders
Architecture progress must be translated into business terms that stakeholders understand.
// lib/architecture-reporting/stakeholder-report.ts
// Generate stakeholder-friendly architecture reports
interface StakeholderReport {
period: { start: Date; end: Date };
executiveSummary: string;
businessImpact: BusinessImpact[];
initiatives: InitiativeSummary[];
metrics: MetricsSummary;
nextPeriodFocus: string[];
}
interface BusinessImpact {
area: string;
impact: string;
metrics: { name: string; before: string; after: string; improvement: string }[];
}
interface InitiativeSummary {
title: string;
status: string;
progress: number;
businessValue: string;
nextMilestone: string;
}
function generateStakeholderReport(
initiatives: ArchitectureInitiative[],
metrics: ArchitectureHealthMetrics,
previousMetrics: ArchitectureHealthMetrics
): StakeholderReport {
const period = {
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
end: new Date(),
};
// Calculate business impacts
const businessImpact: BusinessImpact[] = [];
// Build time → Developer productivity
const buildImprovement =
((previousMetrics.build.averageBuildTime - metrics.build.averageBuildTime) /
previousMetrics.build.averageBuildTime) * 100;
if (Math.abs(buildImprovement) > 5) {
businessImpact.push({
area: 'Developer Productivity',
impact: buildImprovement > 0
? 'Faster feedback loops enable quicker iteration'
: 'Slower builds impacting development velocity',
metrics: [{
name: 'Build Time',
before: `${Math.round(previousMetrics.build.averageBuildTime)}s`,
after: `${Math.round(metrics.build.averageBuildTime)}s`,
improvement: `${Math.abs(buildImprovement).toFixed(0)}% ${buildImprovement > 0 ? 'faster' : 'slower'}`,
}],
});
}
// Coupling → Feature delivery speed
const couplingImprovement =
((previousMetrics.coupling?.cyclicDependencies || 0) -
(metrics.coupling?.cyclicDependencies || 0));
if (couplingImprovement !== 0) {
businessImpact.push({
area: 'Feature Delivery',
impact: couplingImprovement > 0
? 'Reduced dependencies enable parallel feature development'
: 'Increased coupling may slow future feature work',
metrics: [{
name: 'Cyclic Dependencies',
before: `${previousMetrics.coupling?.cyclicDependencies || 0}`,
after: `${metrics.coupling?.cyclicDependencies || 0}`,
improvement: couplingImprovement > 0
? `${couplingImprovement} eliminated`
: `${Math.abs(couplingImprovement)} added`,
}],
});
}
// Dependencies → Security posture
if (metrics.dependencies.vulnerableCount > 0 ||
previousMetrics.dependencies.vulnerableCount > 0) {
const vulnChange = previousMetrics.dependencies.vulnerableCount -
metrics.dependencies.vulnerableCount;
businessImpact.push({
area: 'Security',
impact: vulnChange >= 0
? 'Reduced security vulnerabilities in dependencies'
: 'New security vulnerabilities require attention',
metrics: [{
name: 'Vulnerable Dependencies',
before: `${previousMetrics.dependencies.vulnerableCount}`,
after: `${metrics.dependencies.vulnerableCount}`,
improvement: vulnChange >= 0
? `${vulnChange} fixed`
: `${Math.abs(vulnChange)} new`,
}],
});
}
// Initiative summaries
const initiativeSummaries: InitiativeSummary[] = initiatives
.filter(i => i.status !== 'abandoned')
.map(i => {
const progress = calculateInitiativeProgress(i);
const nextMilestone = i.milestones.find(m => m.status !== 'completed');
// Translate to business value
let businessValue = '';
if (i.title.toLowerCase().includes('decouple')) {
businessValue = 'Enables faster independent feature development';
} else if (i.title.toLowerCase().includes('performance')) {
businessValue = 'Improves user experience and reduces infrastructure costs';
} else if (i.title.toLowerCase().includes('security')) {
businessValue = 'Reduces risk of security incidents';
} else {
businessValue = 'Improves system maintainability and reliability';
}
return {
title: i.title,
status: progress.onTrack ? '🟢 On Track' : progress.riskLevel === 'high' ? '🔴 At Risk' : '🟡 Needs Attention',
progress: Math.round(progress.overallProgress),
businessValue,
nextMilestone: nextMilestone?.title || 'Completing final validation',
};
});
// Executive summary
const completedThisPeriod = initiatives.filter(i =>
i.status === 'completed' &&
i.timeline.actualCompletionDate &&
i.timeline.actualCompletionDate >= period.start
).length;
const healthScore = calculateHealthScore(metrics, previousMetrics);
const executiveSummary = `
Architecture health is ${healthScore.overall >= 70 ? 'good' : healthScore.overall >= 50 ? 'moderate' : 'concerning'} (${healthScore.overall.toFixed(0)}/100, ${healthScore.trend}).
${completedThisPeriod > 0 ? `Completed ${completedThisPeriod} architecture initiative(s) this period.` : ''}
${initiatives.filter(i => i.status === 'in-progress').length} initiative(s) in progress.
Key wins: ${businessImpact.filter(b => b.metrics[0].improvement.includes('faster') || b.metrics[0].improvement.includes('fixed') || b.metrics[0].improvement.includes('eliminated')).map(b => b.area.toLowerCase()).join(', ') || 'Maintaining stability'}.
${healthScore.alerts.filter(a => a.severity === 'critical').length > 0 ? `\n⚠️ ${healthScore.alerts.filter(a => a.severity === 'critical').length} critical issue(s) require attention.` : ''}
`.trim();
return {
period,
executiveSummary,
businessImpact,
initiatives: initiativeSummaries,
metrics: {
healthScore: healthScore.overall,
trend: healthScore.trend,
criticalAlerts: healthScore.alerts.filter(a => a.severity === 'critical').length,
},
nextPeriodFocus: initiatives
.filter(i => i.status === 'in-progress')
.flatMap(i => i.milestones.filter(m => m.status === 'in-progress' || m.status === 'pending').slice(0, 1))
.map(m => m.title),
};
}
// Format as Slack message
function formatSlackReport(report: StakeholderReport): object {
return {
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: '📊 Architecture Health Report',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: report.executiveSummary,
},
},
{
type: 'divider',
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Business Impact This Period*',
},
},
...report.businessImpact.map(impact => ({
type: 'section',
text: {
type: 'mrkdwn',
text: `*${impact.area}*: ${impact.impact}\n${impact.metrics.map(m => `• ${m.name}: ${m.before} → ${m.after} (${m.improvement})`).join('\n')}`,
},
})),
{
type: 'divider',
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Active Initiatives*',
},
},
...report.initiatives.map(init => ({
type: 'section',
text: {
type: 'mrkdwn',
text: `${init.status} *${init.title}* (${init.progress}%)\n_${init.businessValue}_\nNext: ${init.nextMilestone}`,
},
})),
],
};
}
Defining "Done" for Architecture Work
Architecture work is "done" when it achieves its measurable goals—not when the code is merged.
// lib/architecture-initiatives/definition-of-done.ts
// Structured definition of done for architecture work
interface ArchitectureDoD {
// Code changes are complete
codeComplete: {
allPRsMerged: boolean;
noWorkInProgressBranches: boolean;
featureFlagsRemoved: boolean;
};
// Metrics targets achieved
metricsAchieved: {
allCriteriaMet: boolean;
metricsStableForDays: number; // e.g., 14 days
noRegressions: boolean;
};
// Documentation updated
documentationComplete: {
architectureDecisionRecorded: boolean; // ADR
diagramsUpdated: boolean;
runbooksUpdated: boolean;
onboardingDocsUpdated: boolean;
};
// Knowledge transferred
knowledgeTransferred: {
teamWalkthroughCompleted: boolean;
ownershipAssigned: boolean;
supportRotationUpdated: boolean;
};
// Operational readiness
operationalReady: {
monitoringInPlace: boolean;
alertsConfigured: boolean;
rollbackPlanDocumented: boolean;
incidentResponseUpdated: boolean;
};
// Stakeholder sign-off
signedOff: {
technicalLeadApproved: boolean;
productOwnerAcknowledged: boolean;
securityReviewComplete: boolean;
};
}
function checkDefinitionOfDone(
initiative: ArchitectureInitiative
): {
complete: boolean;
checklist: Array<{ item: string; complete: boolean; blocker?: string }>;
readyForClosure: boolean;
} {
const checklist: Array<{ item: string; complete: boolean; blocker?: string }> = [];
// Check milestones
const milestonesComplete = initiative.milestones.every(m => m.status === 'completed');
checklist.push({
item: 'All milestones completed',
complete: milestonesComplete,
blocker: milestonesComplete ? undefined : `${initiative.milestones.filter(m => m.status !== 'completed').length} milestones remaining`,
});
// Check success criteria
const criteriaProgress = initiative.successCriteria.map(c => {
const range = c.baseline - c.target;
const progress = (c.baseline - c.current) / range;
return { ...c, achieved: progress >= 0.95 }; // Within 5% of target
});
const allCriteriaMet = criteriaProgress.every(c => c.achieved);
checklist.push({
item: 'All success criteria met',
complete: allCriteriaMet,
blocker: allCriteriaMet ? undefined : `${criteriaProgress.filter(c => !c.achieved).map(c => c.metric).join(', ')} not yet at target`,
});
// Check documentation
// These would be verified against actual files/systems
const adrExists = checkADRExists(initiative.id);
checklist.push({
item: 'Architecture Decision Record created',
complete: adrExists,
});
const diagramsUpdated = checkDiagramsUpdated(initiative.id);
checklist.push({
item: 'Architecture diagrams updated',
complete: diagramsUpdated,
});
// Check monitoring
const monitoringInPlace = checkMonitoringExists(initiative.id);
checklist.push({
item: 'Monitoring and alerts configured',
complete: monitoringInPlace,
});
// Check knowledge transfer
const ownershipClear = checkOwnershipAssigned(initiative.id);
checklist.push({
item: 'Ownership assigned and documented',
complete: ownershipClear,
});
const complete = checklist.every(item => item.complete);
// Ready for closure if complete and metrics stable
const metricsStable = checkMetricsStability(initiative, 14); // 14 days
const readyForClosure = complete && metricsStable;
return { complete, checklist, readyForClosure };
}
// Prevent "zombie" architecture work
function detectZombieInitiatives(
initiatives: ArchitectureInitiative[]
): Array<{
initiative: ArchitectureInitiative;
reason: string;
recommendation: string;
}> {
const zombies: Array<{
initiative: ArchitectureInitiative;
reason: string;
recommendation: string;
}> = [];
const now = new Date();
for (const initiative of initiatives) {
if (initiative.status !== 'in-progress') continue;
// Check for stalled progress
const lastCompletedMilestone = initiative.milestones
.filter(m => m.completedDate)
.sort((a, b) => b.completedDate!.getTime() - a.completedDate!.getTime())[0];
if (lastCompletedMilestone) {
const daysSinceProgress = Math.floor(
(now.getTime() - lastCompletedMilestone.completedDate!.getTime()) /
(1000 * 60 * 60 * 24)
);
if (daysSinceProgress > 30) {
zombies.push({
initiative,
reason: `No milestone completed in ${daysSinceProgress} days`,
recommendation: 'Review scope and blockers, consider descoping or abandoning',
});
}
}
// Check for missed deadlines
if (initiative.timeline.targetCompletionDate &&
initiative.timeline.targetCompletionDate < now) {
const daysOverdue = Math.floor(
(now.getTime() - initiative.timeline.targetCompletionDate.getTime()) /
(1000 * 60 * 60 * 24)
);
if (daysOverdue > 14) {
zombies.push({
initiative,
reason: `${daysOverdue} days past target completion date`,
recommendation: 'Re-estimate, communicate new timeline, or cut scope',
});
}
}
// Check for blocked milestones
const blockedMilestones = initiative.milestones.filter(m => m.status === 'blocked');
if (blockedMilestones.length > 0) {
zombies.push({
initiative,
reason: `${blockedMilestones.length} blocked milestone(s)`,
recommendation: 'Escalate blockers or re-plan around them',
});
}
// Check for scope creep (too many milestones added)
if (initiative.milestones.length > 8) {
zombies.push({
initiative,
reason: 'Scope creep detected (>8 milestones)',
recommendation: 'Split into multiple focused initiatives',
});
}
}
return zombies;
}
Automated Metrics Collection
# .github/workflows/architecture-metrics.yml
name: Architecture Metrics
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly on Monday
jobs:
collect-metrics:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for analysis
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Collect coupling metrics
run: |
npx tsx scripts/metrics/coupling.ts > metrics/coupling.json
- name: Collect build metrics
run: |
# Time a clean build
START=$(date +%s)
npm run build
END=$(date +%s)
echo "{\"buildTime\": $((END-START))}" > metrics/build.json
- name: Collect test metrics
run: |
npm run test -- --coverage --json --outputFile=metrics/test.json
- name: Collect dependency metrics
run: |
npm audit --json > metrics/audit.json || true
npm outdated --json > metrics/outdated.json || true
- name: Calculate health score
run: |
npx tsx scripts/metrics/health-score.ts
- name: Store metrics
uses: actions/upload-artifact@v4
with:
name: architecture-metrics
path: metrics/
- name: Update metrics dashboard
run: |
npx tsx scripts/metrics/update-dashboard.ts
- name: Post to Slack (weekly)
if: github.event_name == 'schedule'
run: |
npx tsx scripts/reporting/slack-report.ts
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Create issue for critical alerts
if: always()
run: |
npx tsx scripts/reporting/create-alert-issues.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Key Takeaways
-
Measure What Matters: Coupling, build time, test health, and dependency health are leading indicators. Feature delivery time and bug rates are lagging indicators. Track both.
-
Define Success Criteria Upfront: Every architecture initiative needs measurable targets. "Reduce coupling" is a direction; "Reduce cyclic dependencies from 12 to 0" is a target.
-
Milestones with Deliverables: Break architecture work into concrete milestones with tangible deliverables. "Event schema documented" is verifiable; "work on events" is not.
-
Translate to Business Impact: Stakeholders don't care about coupling scores. They care about faster feature delivery, fewer bugs, and lower risk. Connect metrics to outcomes.
-
Definition of Done Includes Operations: Code merged is not done. Metrics achieved, documentation updated, monitoring in place, and ownership assigned is done.
-
Detect Zombie Initiatives: Architecture work that's perpetually "in progress" drains credibility. Set deadlines, track progress, and have the courage to descope or abandon.
-
Automate Metrics Collection: Manual metrics are stale metrics. Automate collection in CI, visualize trends, and alert on regressions.
-
Regular Stakeholder Communication: Weekly or bi-weekly architecture health reports maintain visibility. When stakeholders see progress, they invest more.
The goal is to make architecture work as concrete as feature work. Measurable goals, visible progress, and clear completion criteria transform architecture from an ongoing expense into a series of valuable investments with demonstrable returns.
What did you think?