Skip to main content

Code Health Metrics and Refactoring Strategy

Track cyclomatic complexity, coupling, and technical debt with automated static analysis; prioritize refactoring by impact and effort.

TL;DR

Code health metrics quantify maintainability: cyclomatic complexity (>10 is refactor-candidate), coupling (dependencies between modules), cohesion (methods working together), test coverage (>80% target). CRAP score (Change Risk Analysis and Predictions) combines complexity + coverage. Automate with SonarQube, Pylint, ESLint in CI; fail builds on metric regressions. Refactor based on impact-effort matrix: high-impact, low-effort changes first. Technical debt compounds; track with tools and pay down systematically rather than allowing unbounded accumulation.

Learning Objectives

By the end of this article, you'll understand:

  • Key code metrics: complexity, coupling, cohesion, coverage
  • Identifying code smells and refactoring opportunities
  • Technical debt tracking and prioritization
  • Static analysis tools and integration
  • Refactoring strategies and risk management
  • Metrics as leading indicators of quality issues

Motivating Scenario

Your 200-line payment processing function has 47 different code paths, no tests, and hasn't been touched in 18 months. When you need to add new payment methods, you spend 4 days understanding it. A junior dev's mistake causes a month of debugging. You track that 80% of production bugs come from 10% of your codebase. You need visibility into which modules are high-risk, automated metrics to catch degradation, and a systematic refactoring strategy.

Core Concepts

Cyclomatic Complexity

Count of decision paths in code. Each if/else, loop, switch case adds 1.

  • 1-5: Simple, low risk
  • 6-10: Moderate, consider refactoring
  • 11-20: High risk, refactor
  • 20: Very high risk, must refactor

Example:

def process_order(order):  # Complexity 1
if order.status == "pending": # +1 = 2
if order.amount > 1000: # +1 = 3
# ... apply discount
else: # +1 = 4
# ... normal price
elif order.status == "cancelled": # +1 = 5
# ... handle cancellation
else: # +1 = 6
# ... error

Coupling and Cohesion

Coupling: Number of dependencies on other modules. High coupling makes changes risky.

Cohesion: Do methods in a class work together? High cohesion means focused responsibility.

Target: Low coupling, high cohesion.

CRAP Score

Change Risk Analysis and Predictions = Complexity * (1 - Coverage%) + Complexity²

Combines complexity and test coverage. High CRAP = refactor + add tests.

Technical Debt

Metaphor for accumulating shortcuts. Paying interest (extra effort each change). Principal (time to fix). Track items and prioritize by risk/effort.

Practical Example

# sonar-project.properties

sonar.projectKey=order-service
sonar.projectName=Order Service
sonar.projectVersion=1.0
sonar.sources=src
sonar.tests=test
sonar.sourceEncoding=UTF-8

# Coverage
sonar.coverage.exclusions=**/config/**,**/entity/**
sonar.coverageReportPaths=target/coverage-report.xml
sonar.test.inclusions=**/*Test.java,**/*Tests.java

# Quality Gate: Fail build if metrics degrade
sonar.qualitygate.wait=true

# Complexity threshold
sonar.java.complexityThreshold=10

# Coverage minimum
sonar.coverage.threshold=80

# Rules
sonar.inclusions=**/*.java

# Exclusions
sonar.exclusions=**/vendor/**,**/node_modules/**

# Specific rules to enable/disable
sonar.issue.ignore.multicriteria=e1
sonar.issue.ignore.multicriteria.e1.ruleKey=java:S2095
sonar.issue.ignore.multicriteria.e1.resourceKey=**/test/**
# GitHub Actions: SonarQube scan
name: Code Quality

on: [push, pull_request]

jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Build and test
run: mvn clean verify

- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v2
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

- name: Quality Gate Check
uses: SonarSource/sonarqube-quality-gate-action@v1
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

When to Use / When Not to Use

Refactor First When:
  1. High complexity + low coverage (CRAP score > 20)
  2. Frequent bug source (>30% of defects)
  3. Blocking new features (tight coupling)
  4. Team onboarding friction (hard to understand)
  5. Performance bottlenecks (complex algorithms)
Defer Refactoring When:
  1. Code is stable + not touched frequently
  2. Low complexity + high coverage already
  3. Critical deadline approaching
  4. Uncertain requirements (may change soon)
  5. Low-visibility, low-risk module

Patterns & Pitfalls

Design Review Checklist

  • Cyclomatic complexity tracked (target <10 per function)
  • CRAP score < 20 for critical paths
  • Test coverage > 80% (measure regularly)
  • Quality gates automated in CI (fail on regression)
  • Code smells detected (static analysis enabled)
  • Coupling minimized (low interdependencies)
  • Technical debt tracked and prioritized
  • Refactoring estimated in story points (planned)
  • Metrics dashboard visible to team
  • Dead code cleaned up regularly

Self-Check

Ask yourself:

  • What's the highest complexity function in my codebase?
  • Which 10% of code causes 80% of bugs?
  • Is my test coverage increasing or decreasing?
  • Do I measure CRAP scores?
  • Is technical debt explicit or hidden?

One Key Takeaway

info

Code metrics quantify maintainability; static analysis catches issues early. Automate quality gates to prevent debt accumulation. Refactor based on impact-effort matrix: prioritize high-complexity, low-coverage, frequently-changed code. Small, regular refactoring beats crisis rewrites.

Next Steps

  1. Enable static analysis - SonarQube, ESLint, Pylint
  2. Measure baseline - Complexity, coverage, coupling
  3. Set thresholds - Max complexity, min coverage
  4. Automate gates - Fail builds on regression
  5. Prioritize - Refactor highest CRAP scores first
  6. Track debt - Dashboard of metrics
  7. Review regularly - Trends and patterns

References