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.
Converted and distributed by TomeVault — claim your Tome and manage your conversions.