Technical Debt Patterns
Expert patterns for detecting, categorizing, and prioritizing technical debt in Rails applications.
Decision Tree: Debt Category Identification
What type of issue is this? │ ├─ Code structure problem? │ ├─ Method too long (>20 lines) → Code Smell: Long Method │ ├─ Class too large (>150 lines) → Code Smell: Large Class │ ├─ Excessive parameter passing → Code Smell: Data Clump │ └─ Method uses another object's data → Code Smell: Feature Envy │ ├─ Complexity issue? │ ├─ Flog score >60 → Complexity: High │ ├─ Cyclomatic complexity >10 → Complexity: High │ ├─ Deep nesting (>3 levels) → Complexity: Nesting │ └─ Too many conditionals → Complexity: Conditional │ ├─ Security concern? │ ├─ SQL injection risk → Security: SQL Injection │ ├─ XSS vulnerability → Security: XSS │ ├─ Mass assignment issue → Security: Mass Assignment │ └─ Outdated gem with CVE → Security: Dependency │ ├─ Outdated code? │ ├─ Rails deprecation warning → Deprecation: Rails │ ├─ Ruby version warning → Deprecation: Ruby │ └─ Deprecated gem API → Deprecation: Gem │ ├─ Performance problem? │ ├─ N+1 query pattern → Performance: N+1 │ ├─ Missing database index → Performance: Index │ ├─ Memory bloat → Performance: Memory │ └─ Slow query → Performance: Query │ └─ Architecture violation? ├─ Fat controller → Architecture: Controller ├─ God object/model → Architecture: God Object ├─ Circular dependency → Architecture: Circular └─ Layer violation → Architecture: Layering
NEVER Do These (Critical Anti-Patterns)
NEVER ignore security debt because "we'll fix it later":
WRONG - Ignoring SQL injection
User.where("name = '#{params[:name]}'") # Security debt accumulates risk
RIGHT - Fix immediately or track with Critical severity
User.where(name: params[:name])
→ Security debt has exponential risk growth. Track as Critical, not backlog.
NEVER create technical debt to "fix" technical debt:
WRONG - Adding wrapper to hide complexity
class PaymentWrapper def process @legacy_payment.complex_legacy_method # Just hiding the problem end end
RIGHT - Either refactor properly or track explicitly
If time-constrained, create beads issue with clear scope
→ Debt wrappers compound into "debt squared". Refactor or track, don't hide.
NEVER disable linters globally to silence debt warnings:
WRONG - Global disable in .rubocop.yml
Metrics/MethodLength: Enabled: false # Hides ALL long method debt
RIGHT - Explicit inline disable with justification
rubocop:disable Metrics/MethodLength -- Legacy payment processor, tracked in PROJ-123
def complex_legacy_method
...
end
rubocop:enable Metrics/MethodLength
→ Global disables hide debt accumulation. Use inline disables with tracking.
NEVER skip team discussion on Critical/High debt:
WRONG - Solo decision on major debt
"I found a god object, I'll refactor it this sprint"
RIGHT - Team alignment first
- Document finding in debt report
- Create beads issue with severity
- Discuss in sprint planning
- Get consensus on approach
→ Major refactoring affects the whole team. Collaborate before large changes.
NEVER estimate features without considering debt in affected areas:
WRONG - Ignoring debt in estimates
"Add payment retry logic: 2 story points"
RIGHT - Include debt impact
"Add payment retry logic: 5 story points
- 2 points: feature implementation
- 3 points: PaymentService complexity (Flog 127) requires refactoring first"
→ Debt adds hidden cost. Include remediation in feature estimates.
Code Smell Detection Patterns
Long Method (>20 lines)
Detection:
Find methods longer than 20 lines
awk ' /^[[:space:]]*def / { start = NR; name = $2 } /^[[:space:]]end/ && start > 0 { len = NR - start if (len > 20) print FILENAME ":" name " (" len " lines)" start = 0 } ' app/**/.rb
Severity Thresholds:
Lines Severity
20-40 Medium
40-80 High
80 Critical
Large Class (>150 lines)
Detection:
Find classes larger than 150 lines
for file in app/models/.rb app/services/.rb; do lines=$(wc -l < "$file" 2>/dev/null) if [ "$lines" -gt 150 ]; then echo "$file: $lines lines" fi done
Severity Thresholds:
Lines Severity
150-300 Medium
300-500 High
500 Critical
Feature Envy
Method uses another object's data more than its own.
Detection Pattern:
Smell: Method calls another object's methods repeatedly
def calculate_total(order) order.items.sum(&:price) + order.shipping_cost + order.tax_amount - order.discount_amount end
Fix: Move method to Order class
class Order def calculate_total items.sum(&:price) + shipping_cost + tax_amount - discount_amount end end
Data Clump
Same group of parameters passed together repeatedly.
Detection Pattern:
Smell: Repeated parameter group
def create_user(name, email, phone, address) def update_user(name, email, phone, address) def validate_user(name, email, phone, address)
Fix: Extract to value object
class ContactInfo attr_reader :name, :email, :phone, :address end
def create_user(contact_info) def update_user(contact_info)
Complexity Metrics
Flog Score Thresholds
Score Rating Action
< 30 Low No action needed
30-60 Medium Consider refactoring
60-100 High Plan refactoring
100 Critical Immediate refactoring
Running Flog:
Overall complexity score
flog -q -g app/
Top 10 most complex methods
flog -q app/ | head -10
Score for specific file
flog app/services/payment_service.rb
Cyclomatic Complexity
Measures independent execution paths through code.
Complexity Rating Risk
1-5 Low Easy to test
6-10 Medium Moderate risk
11-20 High Difficult to test
20 Critical Very high risk
Severity Scoring Framework
Calculate overall severity using weighted factors:
Severity Score = (Blast × 0.30) + (Fix × 0.20) + (Risk × 0.30) + (Age × 0.10) + (Freq × 0.10)
Factor Weight 1 (Low) 3 (Medium) 5 (High)
Blast Radius 30% Single file Module System-wide
Fix Complexity 20% Trivial Moderate Major refactor
Risk Level 30% Cosmetic Functional Security/Data
Age 10% < 6 months 6mo-2yr
2 years
Frequency 10% Rare path Normal Hot path
Severity Categories:
-
Critical: Score >= 4.0 (SLA: 1 sprint)
-
High: Score >= 3.0 (SLA: 2 sprints)
-
Medium: Score >= 2.0 (SLA: Quarterly)
-
Low: Score < 2.0 (Opportunistic)
Quick Reference Tables
Detection Tools
Tool Purpose Command
Flog Complexity scoring flog -q app/
Reek Code smell detection reek app/
Rubocop Style + metrics rubocop --format json
Brakeman Security vulnerabilities brakeman -q
bundler-audit Gem CVEs bundle-audit check
rails_best_practices Rails anti-patterns rails_best_practices
Effort Estimation
Category Typical Effort
Long Method refactor 2-4 hours
Large Class extraction 1-3 days
God Object decomposition 3-5 days
N+1 query fix 1-2 hours
Security vulnerability 2-8 hours
Deprecation update 2-4 hours
Circular dependency fix 2-5 days
Beads Integration
Create debt issue
bd create --type task --priority 1
--title "Tech Debt: [Description]"
--description "[Details]"
Track debt item
bd update PROJ-123 --status in_progress
Close after remediation
bd close PROJ-123 --reason "Refactored in commit abc123"
References
For detailed patterns in each category, see:
-
references/code-smells.md
-
Long method, large class, feature envy, data clump
-
references/complexity-metrics.md
-
Flog, cyclomatic, cognitive complexity
-
references/deprecation-tracking.md
-
Rails, Ruby, gem deprecations
-
references/security-debt.md
-
Brakeman categories, OWASP patterns
-
references/performance-debt.md
-
N+1 queries, indexes, memory
-
references/testing-debt.md
-
Coverage gaps, flaky tests
-
references/architecture-debt.md
-
God objects, circular deps, layers
Integration with Other Skills
Skill Integration Point
code-quality-gates
Rubocop/Sorbet findings feed into debt report
refactoring-workflow
Debt items become refactoring targets
rails-conventions
Convention violations are architecture debt
codebase-inspection
Inspector discovers debt during analysis