The Refactor That Brought Down Production
I once refactored 50 files in three days. Everything looked cleaner. I merged to production feeling proud.
Ten minutes later, PagerDuty exploded. Critical features broken. Users complaining.
What went wrong: I refactored without understanding the code first. That “ugly” code was handling edge cases I didn’t know existed.
The lesson: Legacy code survived in production for a reason. Respect it.
The Mindset Shift
Wrong mindset: “This code is terrible. The previous dev didn’t know what they were doing.”
Right mindset: “This code works in production. There’s probably a reason it’s written this way.”
That “hacky” solution might fix a critical bug. That “redundant” check might prevent a crash. That “weird” pattern might handle an edge case.
Understand WHY before changing WHAT.
The Safe Refactoring Process
Step 1: Understand First
Check git history to see why code was added and what bugs were fixed. Look for existing tests. Run the code with a debugger. Try different inputs.
Don’t refactor until you understand what you’re refactoring.
Step 2: Add Tests First
Never refactor without tests. If you have 200 lines of complex logic with no tests, write tests first. Cover all the edge cases you can find. Then refactor.
Tests tell you if you broke something.
Step 3: Small Steps
Don’t make giant refactoring PRs with 47 files changed. Make many small PRs. Each one changes one thing, is easy to review, and can be rolled back independently.
Step 4: The Strangler Fig Pattern
For major refactors, don’t rewrite everything at once. Create a new implementation alongside the old one. Use feature flags to route 5% of traffic to the new code. Gradually increase to 25%, 50%, then 100%. Remove the old implementation when confident.
Benefits: You can roll back anytime, test in production safely, and catch issues early.
Step 5: The Boy Scout Rule
Always leave code better than you found it. Fix small things while you’re there - change var to const, add proper spacing, improve naming. Small improvements compound over time.
Common Refactoring Patterns
Extract Function
Break long functions into smaller ones with single responsibilities:
// Before: One long function doing everything
function processOrder(order) {
// validation, calculation, discount, payment all mixed
}
// After: Separate concerns
function processOrder(order) {
validateOrder(order);
const total = calculateTotal(order);
const finalAmount = applyDiscount(total, order.customer);
return processPayment(finalAmount, order.customerId);
}
Replace Magic Numbers
// Before
if (quantity > 100) return quantity * 9.99 * 0.8;
// After
const BULK_THRESHOLD = 100;
const BULK_DISCOUNT = 0.8;
if (quantity > BULK_THRESHOLD) return quantity * UNIT_PRICE * BULK_DISCOUNT;
Simplify Nested Conditionals
Use guard clauses to reduce nesting:
// Before: Nested nightmare
if (user) {
if (user.subscription) {
if (user.subscription.active) {
return user.subscription.plan === 'premium' ? 29.99 : 9.99;
}
}
}
// After: Early returns
if (!user) throw new Error('User not found');
if (!user.subscription || !user.subscription.active) return 0;
return user.subscription.plan === 'premium' ? 29.99 : 9.99;
When NOT to Refactor
Don’t refactor if:
- It works and nobody touches it
- You don’t understand it yet
- No tests exist (and you can’t add them)
- Right before a deadline
- Just because it’s “old” (old doesn’t mean bad)
Tools That Help
Most IDEs support safe refactoring: rename variable, extract function, move to another file. Use them.
ESLint catches common issues. Prettier ensures consistent formatting. TypeScript catches type errors.
The Checklist
Before: Understand the code, check git history, ensure tests exist, have a rollback plan.
During: Make small changes, keep tests passing, commit frequently.
After: All tests pass, manual testing done, code review completed, deploy to staging first, monitor production closely.
Refactoring legacy code is like defusing a bomb.
One wrong move and everything explodes.
Go slow. Understand first. Test everything. Make small changes.
The goal isn’t perfect code. The goal is code that’s slightly better than before, without breaking production.
Legacy code got you here. Respect it. Improve it gradually.
Your future self will thank you.