Identifying, measuring, tracking, and managing technical debt strategically and systematically
Scope: Comprehensive guide to identifying, measuring, prioritizing, and paying down technical debt Lines: ~320 Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)
Activate this skill when:
Definition: Code shortcuts that speed up delivery but increase future maintenance costs
Martin Fowler's Quadrant:
Reckless Prudent
Deliberate | "We don't have | "We must ship now,
| time for | will refactor later"
| design" |
-------------|----------------|------------------
Inadvertent | "What's | "Now we know how
| layering?" | we should have
| | done it"
Types of Tech Debt:
Like Financial Debt:
# Example: Duplicated validation logic (Principal: 1 day to refactor)
# File 1
def create_user(email):
if "@" not in email: # Duplicated
raise ValueError("Invalid email")
# ... save user
# File 2
def update_user(email):
if "@" not in email: # Duplicated
raise ValueError("Invalid email")
# ... update user
# File 3
def invite_user(email):
if "@" not in email: # Duplicated
raise ValueError("Invalid email")
# ... send invite
# Interest paid:
# - Every bug fix needs 3 changes
# - Every validation enhancement needs 3 updates
# - Risk of inconsistency
# - Slows down all email-related features
Good Reasons (Deliberate, Prudent):
Bad Reasons (Reckless):
Code Smells Checklist:
[ ] Duplicated code (copy-paste)
[ ] Long methods (> 30 lines)
[ ] Large classes (> 500 lines)
[ ] Long parameter lists (> 4 params)
[ ] Divergent change (class changes for multiple reasons)
[ ] Shotgun surgery (one change affects many classes)
[ ] Feature envy (method uses another class more than its own)
[ ] Data clumps (same group of data everywhere)
[ ] Primitive obsession (over-reliance on primitives)
[ ] Comments explaining complex code
Automated Detection:
# Python: Code complexity
radon cc src/ -a -nb # Cyclomatic complexity
radon mi src/ # Maintainability index
# JavaScript: ESLint complexity rules
eslint --max-complexity=10 src/
# Code duplication
jscpd src/ # Copy-paste detector
# Security debt
npm audit
pip-audit
Debt Score Formula:
Debt Score = (Complexity × Impact × Frequency) / Ease of Fix
Complexity: 1-5 (how complex is the problem?)
Impact: 1-5 (how many people/systems affected?)
Frequency: 1-5 (how often is this code touched?)
Ease of Fix: 1-5 (how easy to fix?)
Example Scoring:
# Debt Item: Duplicated email validation (3 places)
Complexity = 2 # Simple validation logic
Impact = 4 # Used in 10+ features
Frequency = 5 # Changed weekly
Ease of Fix = 4 # Easy - extract to function
Debt Score = (2 × 4 × 5) / 4 = 10
# Debt Item: Monolithic architecture
Complexity = 5 # Very complex to split
Impact = 5 # Affects entire system
Frequency = 3 # Changes monthly
Ease of Fix = 1 # Very difficult
Debt Score = (5 × 5 × 3) / 1 = 75 # High priority!
Debt Register (track in TECH_DEBT.md):
# Technical Debt Register
## High Priority (Score > 50)
### TD-001: Monolithic Database
- **Debt Score**: 75
- **Principal**: 4 weeks
- **Interest**: 1 day/week (slow queries, deployment bottleneck)
- **Paydown Plan**: Split into microservices over Q2
- **Status**: Planned
## Medium Priority (Score 20-50)
### TD-002: Missing Integration Tests
- **Debt Score**: 30
- **Principal**: 2 weeks
- **Interest**: 2 hours/week (manual testing)
- **Paydown Plan**: Add tests incrementally with each feature
- **Status**: In Progress
## Low Priority (Score < 20)
### TD-003: Outdated Dependencies
- **Debt Score**: 12
- **Principal**: 1 day
- **Interest**: 30 min/month (security patches)
- **Paydown Plan**: Update in Q3 maintenance sprint
- **Status**: Backlog
Boy Scout Rule: Leave code better than you found it
# Every PR: Small improvements
# Before
def process_user(email, name, age, address, phone): # Too many params
# ... 50 lines of code
# After (in PR for unrelated feature)
class UserData:
def __init__(self, email, name, age, address, phone):
self.email = email
self.name = name
# ...
def process_user(user_data: UserData): # Better!
# ... same logic
Debt Sprints: Dedicated time to pay down debt
Sprint 10: Features (80%) + Debt (20%)
- Feature: User dashboard
- Debt: Extract shared validation logic
Sprint 11: Features (80%) + Debt (20%)
- Feature: Payment integration
- Debt: Add integration tests for auth
Sprint 15: Debt Sprint (100%)
- Upgrade all dependencies
- Refactor monolithic service
- Add missing documentation
Strangler Fig Pattern: Gradually replace old system
1. Identify module to replace
2. Build new implementation alongside old
3. Route new requests to new code
4. Migrate existing data gradually
5. Remove old code when fully migrated
Example:
┌─────────────────┐
│ Old Auth System │ ←─┐
└─────────────────┘ │
├─ Route new users to new system
┌─────────────────┐ │ Migrate old users gradually
│ New Auth System │ ←─┘
└─────────────────┘
Definition of Done (include quality checks):
## Definition of Done
Code Complete:
- [ ] Feature implemented
- [ ] Unit tests written (80%+ coverage)
- [ ] Integration tests added
- [ ] Documentation updated
- [ ] Code reviewed
- [ ] No TODO/FIXME comments (create tickets instead)
- [ ] No copy-paste code
- [ ] Cyclomatic complexity < 10
- [ ] No security vulnerabilities
Pre-Commit Hooks:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
hooks:
- id: black # Formatting
- repo: https://github.com/pycqa/flake8
hooks:
- id: flake8 # Linting
args: [--max-complexity=10]
- repo: https://github.com/pre-commit/mirrors-mypy
hooks:
- id: mypy # Type checking
Quality Gates in CI:
# .github/workflows/quality.yml
- name: Check code quality
run: |
radon cc src/ -a -nb --total-average-threshold=B
radon mi src/ --min=C
- name: Check test coverage
run: |
pytest --cov=src --cov-fail-under=80
- name: Check for code duplication
run: |
jscpd src/ --threshold 5 # Fail if > 5% duplication
Avoid: "We have technical debt" (vague, no context)
Better: Use analogies and business impact
"Our authentication system is like a house built without a foundation.
It works today, but:
- Every new feature takes 2x longer to build
- Security fixes require changes in 5 different places
- We have 3 critical bugs per month due to inconsistency
Investment: 2 weeks to refactor
Payoff: 50% faster feature development, 90% fewer auth bugs
ROI: Pays for itself in 2 months"
Debt Dashboard (visualize for stakeholders):
Tech Debt Metrics Dashboard
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Debt: 12 weeks
Interest Cost: 8 hours/week
High Priority Debt: [██████----] 60%
Test Coverage: [████------] 45%
Code Duplication: [██████----] 15%
Complexity: [████████--] 80%
Trend: ↑ Increasing (action needed!)
Do's:
Don'ts:
❌ "We'll fix it later" (Never happens)
→ Fix now or create explicit paydown plan
❌ Ignoring debt until crisis
→ Track and pay down incrementally
❌ Only paying principal, ignoring interest
→ Prioritize high-interest debt first
❌ 100% features, 0% debt paydown
→ Allocate 20% of sprint to debt
❌ Rewriting from scratch
→ Refactor incrementally (strangler fig)
❌ Not measuring debt
→ Quantify to prioritize effectively
❌ Debt as excuse for poor quality
→ Deliberate, prudent debt only
IF time_pressure AND short_term_need AND have_paydown_plan:
TAKE_ON_DEBT
DOCUMENT(why, when_fix, cost)
SCHEDULE_PAYDOWN
ELSE:
DO_IT_RIGHT
Example Decision Matrix: | Situation | Take Debt? | Condition | |-----------|------------|-----------| | MVP launch | ✅ Yes | Must have paydown plan | | Competitive threat | ✅ Yes | Document & schedule fix | | Learning new tech | ✅ Yes | First time doing X | | Lack of skill | ❌ No | Learn or ask for help | | Laziness | ❌ No | Do it right | | No plan to fix | ❌ No | Schedule paydown or don't ship |
# Duplicated validation everywhere
def create_user(email, password):
if not email or "@" not in email:
raise ValueError("Invalid email")
if len(password) < 8:
raise ValueError("Password too short")
# ... create user
def update_email(user_id, email):
if not email or "@" not in email: # Duplicated!
raise ValueError("Invalid email")
# ... update email
def send_invite(email):
if not email or "@" not in email: # Duplicated!
raise ValueError("Invalid email")
# ... send invite
# Extracted validation (DRY)
class EmailValidator:
@staticmethod
def validate(email: str) -> None:
if not email or "@" not in email:
raise ValueError("Invalid email")
class PasswordValidator:
@staticmethod
def validate(password: str) -> None:
if len(password) < 8:
raise ValueError("Password too short")
def create_user(email, password):
EmailValidator.validate(email)
PasswordValidator.validate(password)
# ... create user
def update_email(user_id, email):
EmailValidator.validate(email)
# ... update email
def send_invite(email):
EmailValidator.validate(email)
# ... send invite