Refactoring Workflow
Step-by-step guide for safely refactoring code without breaking functionality in any codebase.
When to Use This Skill
Use when:
- •User asks "How do I refactor this code?" or "Can I simplify this?"
- •Reducing code duplication (DRY violations)
- •Simplifying complex functions (>50 lines or cyclomatic complexity >10)
- •Extracting reusable logic
- •Improving code structure or naming
- •Addressing technical debt
- •User mentions: "refactor", "clean up", "simplify", "reduce duplication"
Do NOT use for:
- •Style preferences without measurable benefit
- •Just before deadlines
- •Code without existing tests
- •Changes that alter behavior (that's feature work, not refactoring)
Core Principles
- •Test-Driven: Tests must exist BEFORE refactoring
- •Incremental: One change at a time, test after each
- •Behavioral Preservation: Same inputs → same outputs
- •Reversible: Each step committed separately for rollback
- •Measurable: Complexity reduced, duplication eliminated
Prerequisites (CRITICAL)
NEVER refactor without tests!
# Check test coverage (examples for different languages) npm test -- --coverage # JavaScript/TypeScript (Jest/Vitest) python -m pytest --cov # Python (pytest) cargo test # Rust go test -cover ./... # Go dotnet test /p:CollectCoverage=true # C# # If coverage < 80% for code being refactored: # 1. STOP # 2. Write tests FIRST # 3. THEN refactor
Minimum requirements:
- • Unit tests exist for all functions being changed
- • Integration tests exist for workflows being changed
- • All tests currently passing ✅
Phase 1: Preparation
Step 1: Document Current Behavior
Create temporary documentation:
# Refactoring: [Module/Function Name] ## Current Behavior - Input: X - Output: Y - Side effects: Z - Edge cases: A, B, C ## Existing Tests - test_case_1: normal flow - test_case_2: error handling - test_case_3: edge case ## Success Criteria After refactor: all tests pass, same behavior
Step 2: Create Safety Backup
# Create backup branch git checkout -b backup-before-refactor git checkout -b refactor/improve-module-x
Step 3: Create Refactoring Checklist
## Refactoring Checklist ### Before - [ ] All existing tests pass - [ ] Coverage documented - [ ] Behavior documented - [ ] Backup branch created ### During - [ ] ONE change at a time - [ ] Run tests after EACH change - [ ] Commit after EACH success ### After - [ ] All tests still pass - [ ] No errors in test output - [ ] Manual testing complete - [ ] Performance unchanged/better - [ ] Documentation updated
Phase 2: Refactoring Patterns
Pattern A: Extract Function (Reduce Complexity)
When: Function >50 lines or cyclomatic complexity >10
Before (TypeScript example):
async function processOrder(order: Order) {
// Validation (10 lines)
if (!order.customerId) throw new Error('Customer required')
if (!order.items || order.items.length === 0) throw new Error('Items required')
if (order.total < 0) throw new Error('Invalid total')
// Calculate tax (15 lines)
let tax = 0
for (const item of order.items) {
const rate = item.taxable ? 0.08 : 0
tax += item.price * item.quantity * rate
}
// Save (10 lines)
const finalOrder = { ...order, tax, grandTotal: order.total + tax }
return await database.orders.create(finalOrder)
}
After:
async function processOrder(order: Order) {
validateOrder(order)
const tax = calculateTax(order.items)
return await saveOrder(order, tax)
}
function validateOrder(order: Order) {
if (!order.customerId) throw new Error('Customer required')
if (!order.items || order.items.length === 0) throw new Error('Items required')
if (order.total < 0) throw new Error('Invalid total')
}
function calculateTax(items: OrderItem[]): number {
return items.reduce((sum, item) => {
const rate = item.taxable ? 0.08 : 0
return sum + (item.price * item.quantity * rate)
}, 0)
}
async function saveOrder(order: Order, tax: number) {
const grandTotal = order.total + tax
return await database.orders.create({ ...order, tax, grandTotal })
}
Python equivalent:
# Before
def process_order(order):
# Validation
if not order.customer_id:
raise ValueError('Customer required')
# Tax calculation
tax = sum(item.price * item.quantity * 0.08
for item in order.items if item.taxable)
# Save
return db.orders.create({**order, 'tax': tax})
# After
def process_order(order):
validate_order(order)
tax = calculate_tax(order.items)
return save_order(order, tax)
def validate_order(order):
if not order.customer_id:
raise ValueError('Customer required')
if not order.items:
raise ValueError('Items required')
def calculate_tax(items):
return sum(item.price * item.quantity * 0.08
for item in items if item.taxable)
def save_order(order, tax):
return db.orders.create({**order, 'tax': tax})
Steps:
- •Extract ONE function at a time
- •Run tests after each extraction
- •Commit each success
- •Add tests for new functions
Pattern B: Extract Utility (Eliminate Duplication)
When: Same logic duplicated across 3+ locations
Before (JavaScript example - 5 files with this code):
const imageUrl = data?.image?.url || data?.image?.file_url || '/placeholder.png'
After:
// utils/image.js
export function getImageUrl(imageData, fallback = '/placeholder.png') {
if (!imageData) return fallback
return imageData.url || imageData.file_url || fallback
}
// All 5 files now:
const imageUrl = getImageUrl(data?.image)
Rust equivalent:
// Before (duplicated in multiple modules)
let image_url = image_data
.and_then(|img| img.url.or(img.file_url))
.unwrap_or_else(|| "/placeholder.png".to_string());
// After
// utils/image.rs
pub fn get_image_url(image_data: Option<&ImageData>) -> String {
image_data
.and_then(|img| img.url.as_ref().or(img.file_url.as_ref()))
.cloned()
.unwrap_or_else(|| "/placeholder.png".to_string())
}
// All modules now:
let image_url = get_image_url(image_data.as_ref());
Steps:
- •Create utility function
- •Write comprehensive tests for utility
- •Replace in ONE location
- •Test
- •Commit
- •Repeat for each remaining location
Pattern C: Replace Magic Numbers/Strings with Constants
When: Same literal value appears 5+ times
Before (Python example):
if user.status == "active":
process_user(user)
if account.status == "active": # Typo risk: "activ", "Active", etc.
charge(account)
After:
# constants.py
USER_STATUS_ACTIVE = "active"
USER_STATUS_INACTIVE = "inactive"
# usage
if user.status == USER_STATUS_ACTIVE:
process_user(user)
if account.status == USER_STATUS_ACTIVE:
charge(account)
Go equivalent:
// Before
if user.Status == "active" {
processUser(user)
}
// After
// constants/user.go
const (
StatusActive = "active"
StatusInactive = "inactive"
)
// usage
if user.Status == constants.StatusActive {
processUser(user)
}
Pattern D: Simplify Complex Conditionals
When: Nested conditions >3 levels deep
Before:
if (user) {
if (user.isActive) {
if (user.subscription) {
if (user.subscription.plan === 'premium') {
return true
}
}
}
}
return false
After:
function hasPremiumSubscription(user: User | null): boolean {
if (!user) return false
if (!user.isActive) return false
if (!user.subscription) return false
return user.subscription.plan === 'premium'
}
// Or with early returns:
function hasPremiumSubscription(user: User | null): boolean {
if (!user || !user.isActive || !user.subscription) {
return false
}
return user.subscription.plan === 'premium'
}
Phase 3: Incremental Execution
CRITICAL WORKFLOW: One change → Test → Commit
# 1. Create working branch git checkout -b refactor/improve-module-x # 2. Make ONE small change # ... edit code ... # 3. Run tests npm test # JavaScript/TypeScript python -m pytest # Python cargo test # Rust go test ./... # Go # 4. If pass, commit git add . git commit -m "refactor: extract validate_order function - Moved validation logic from process_order - All tests passing - No behavior changes" # 5. Repeat for next change # ... edit code ... npm test git commit -m "refactor: extract calculate_tax" # Continue until complete
Testing After EVERY Change
# After each change: # 1. Run tests npm test # JavaScript pytest # Python cargo test # Rust go test ./... # Go dotnet test # C# # 2. Type check (if applicable) npm run type-check # TypeScript mypy . # Python cargo check # Rust # 3. Linting npm run lint # JavaScript/TypeScript pylint src/ # Python cargo clippy # Rust golangci-lint run # Go # If ANY fail: git reset --hard HEAD # Undo # OR fix before committing
Phase 4: Validation
Comprehensive Testing Checklist
Automated:
- • All unit tests pass
- • All integration tests pass
- • Type check passes (if applicable)
- • Linting passes
- • No new warnings
Manual:
- • Feature works exactly as before
- • No errors in logs
- • Performance unchanged/better
- • Edge cases still handled correctly
Code Quality:
- • More readable
- • More testable
- • Less duplication
- • Lower complexity
- • Reasonable function/file sizes
Performance Verification
# Before refactor - run benchmarks npm run bench # JavaScript python -m pytest --benchmark-only # Python (pytest-benchmark) cargo bench # Rust go test -bench=. # Go # After refactor - compare results # Should be similar or better
Phase 5: Documentation
Update Project Docs
If new patterns introduced, update your project's documentation:
## Code Conventions ### Image URL Handling Always use `getImageUrl()` from `utils/image`. Prevents silent failures from null/undefined. **Example:** \`\`\`javascript const url = getImageUrl(data?.image) // ✅ Safe const url = data?.image?.url // ❌ Doesn't handle fallback \`\`\`
Update Changelog
### Session: Refactor Order Processing (Feb 3, 2026) **Goal**: Eliminate complex nested logic in processOrder **Changes**: - Extracted `validateOrder()` function - Extracted `calculateTax()` function - Extracted `saveOrder()` function - Reduced cyclomatic complexity from 12 to 3 **Impact**: - Improved testability (each function tested separately) - Reduced duplication by ~40 lines - Easier to modify validation logic **Validation**: - ✅ All 164 tests pass - ✅ No behavior changes - ✅ Performance unchanged (benchmarked) **Commits**: abc123, def456, ghi789
Anti-Patterns (DON'T DO THIS)
❌ Big Bang Refactor
# WRONG: Change 50 files at once git commit -m "refactor: everything" # CORRECT: Incremental commits git commit -m "refactor: extract validation" git commit -m "refactor: extract calculation" git commit -m "refactor: simplify main function"
❌ Refactor Without Tests
# WRONG: No tests exist
def refactored_function():
# Hope this works! 🤞
pass
# CORRECT: Write tests first
def test_refactored_function():
assert refactored_function(input) == expected
def refactored_function():
# Implementation with confidence
pass
❌ Change Behavior
// WRONG: Added new validation during refactor
function validateOrder(order: Order) {
if (!order.customerId) throw new Error('Customer required')
if (!order.email) throw new Error('Email required') // NEW! Breaks existing code
}
// CORRECT: Preserve exact behavior
function validateOrder(order: Order) {
if (!order.customerId) throw new Error('Customer required')
// Same validation as before, just extracted
}
❌ Premature Optimization
// WRONG: No measured problem, making code complex // Replacing simple readable code with "faster" but unreadable code // CORRECT: Measure first (cargo bench), optimize if needed // Keep code simple unless profiling shows issue
❌ Refactor Under Pressure
// WRONG: "Production deploy tomorrow, let me refactor today!" // CORRECT: Refactor when you have time to test properly
Common Scenarios
Scenario 1: Function Too Large (>50 lines)
Fix:
- •Extract helper functions (one responsibility each)
- •Extract utilities for reusable logic
- •Use descriptive names
- •Test each extracted function
Scenario 2: Duplicated Code (3+ places)
Fix:
- •Identify common pattern
- •Extract to utility function/module
- •Write comprehensive tests
- •Replace one by one
- •Commit after each replacement
Scenario 3: Hard to Test
Fix:
- •Identify external dependencies (DB, API, filesystem)
- •Extract dependencies to parameters
- •Make functions pure (same input → same output)
- •Write tests with mocks/stubs
Scenario 4: Unclear Naming
Fix:
- •Rename ONE identifier at a time
- •Use IDE refactor (F2 in VS Code, Rename in IntelliJ)
- •Run tests
- •Commit
- •Repeat for next name
Emergency Rollback
If refactoring breaks something:
# Option 1: Revert last commit git revert HEAD # Option 2: Restore from backup git checkout backup-before-refactor git checkout -b refactor/v2 # Option 3: Stash and investigate git stash npm test # Pass now? git stash pop # Re-apply and fix # Option 4: Nuclear reset git reset --hard origin/main # Start over with smaller changes
Success Metrics
Refactoring succeeds when:
✅ All tests pass (no behavior changes)
✅ Code more readable (clear improvement)
✅ Complexity reduced (fewer lines, simpler logic, lower cyclomatic complexity)
✅ Duplication removed (DRY)
✅ Test coverage maintained/improved
✅ Performance unchanged/better
✅ No regressions (manual testing confirms)
Language-Specific Testing Commands
# JavaScript/TypeScript npm test npm test -- --coverage npm run type-check npm run lint # Python python -m pytest python -m pytest --cov mypy . pylint src/ # Rust cargo test cargo test --all-features cargo check cargo clippy # Go go test ./... go test -cover ./... go vet ./... golangci-lint run # C# dotnet test dotnet test /p:CollectCoverage=true dotnet build # Ruby bundle exec rspec bundle exec rubocop # Java mvn test ./gradlew test
When to Stop
Stop refactoring when:
- •Tests start failing frequently (too aggressive)
- •Code is "good enough" (perfect is enemy of done)
- •Deadline approaching (commit what you have)
- •No measurable benefit (diminishing returns)
- •You're just tweaking style (not improving structure)
Related Skills
- •tdd-workflow - Write tests before refactoring
- •feature-implementation - Adding features
- •validation-troubleshooting - When tests fail
This skill is framework-agnostic. Patterns apply to JavaScript, TypeScript, Python, Rust, Go, Java, C#, Ruby, or any language.