Legacy Code: The Monster That Feeds You
Published on October 10, 2025
Legacy Code: The Monster That Feeds You
We all have that code. The code you wrote years ago, when you were less experienced, when priorities were different. The code that embarrasses you today, that violates every principle you now know, but that still runs in production, generating revenue, serving customers.
That’s legacy code. And it’s the monster that feeds you.
Every project has legacy code. Code that works but that you wouldn’t write today. Code that embarrasses you but that still generates value.
In this article I’ll share how to manage legacy code professionally: when to refactor, when to leave it alone, and how to work with code you’re not proud of but that’s still valuable.
What is Legacy Code?
Legacy code isn’t just old code. It’s code that:
- Works in production and generates value
- Is hard to understand or modify
- Has no tests (or has poor tests)
- Uses obsolete technologies or patterns
- Was written when requirements were different
But it still works. And that’s what matters.
The Legacy Code Paradox
Legacy code is a paradox:
- It embarrasses you: You know it’s not your best work
- It feeds you: It generates revenue, serves customers
- You want to rewrite it: But rewriting is risky and costly
- You have to maintain it: But maintaining it is hard and frustrating
This tension is real, and managing it correctly is crucial for your project’s success.
When to refactor? When to leave it alone?
Not all legacy code needs to be refactored. Sometimes the best code is the code you don’t touch.
Leave it alone when:
- It works perfectly: If there are no bugs and you don’t need to add features, leave it alone
- There are no planned changes: If you’re not going to modify that part, there’s no reason to refactor
- The cost is high: If refactoring is more expensive than maintaining, don’t do it
- It’s isolated: If the problematic code is isolated and doesn’t affect other parts, leave it
Refactor when:
- You need to add features: If you need to modify legacy code frequently, refactor
- There are frequent bugs: If legacy code causes bugs constantly, refactor
- It’s blocking development: If legacy code makes it hard to add new features, refactor
- You can do it incrementally: If you can refactor without risk, do it
Strategies for Working with Legacy Code
1. The “Strangler Fig” Strategy
Instead of rewriting everything at once, replace parts gradually.
// ❌ Before: Everything in one legacy file
class LegacyOrderProcessor {
processOrder(order) {
// 500 lines of legacy code
// ...
}
}
// ✅ After: Extract parts gradually
class LegacyOrderProcessor {
constructor() {
this.validator = new OrderValidator(); // New
this.calculator = new PriceCalculator(); // New
}
processOrder(order) {
// New validation
this.validator.validate(order);
// New calculation
const total = this.calculator.calculate(order);
// Legacy logic (still)
// ... legacy code that works
}
}
// Eventually, replace completely
class OrderProcessor {
processOrder(order) {
this.validator.validate(order);
const total = this.calculator.calculate(order);
// New clean implementation
}
}
Advantages:
- Low risk: Small, gradual changes
- No downtime: The system keeps working
- Learning: You understand the legacy code as you replace it
2. Add tests before refactoring
Before touching legacy code, add tests that verify current behavior.
// Legacy code (no tests)
function calculateTotal(items, discount) {
// Complex, hard-to-understand logic
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
if (discount > 0) {
total = total * (1 - discount);
}
return Math.round(total * 100) / 100;
}
// ✅ Add tests first
describe('calculateTotal', () => {
it('should calculate total without discount', () => {
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 1 }
];
expect(calculateTotal(items, 0)).toBe(25);
});
it('should apply discount correctly', () => {
const items = [{ price: 100, quantity: 1 }];
expect(calculateTotal(items, 0.1)).toBe(90);
});
// More tests to cover edge cases
});
// Now you can refactor with confidence
function calculateTotal(items, discount) {
const subtotal = items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
const discountedTotal = subtotal * (1 - discount);
return Math.round(discountedTotal * 100) / 100;
}
Advantages:
- Safety: Tests alert you if you break something
- Documentation: Tests document expected behavior
- Confidence: You can refactor without fear
3. Isolate legacy code
If you can’t refactor, at least isolate the legacy code from the rest of the application.
// ❌ Bad: Legacy code mixed with new code
class UserService {
createUser(userData) {
// New, clean code
const user = this.validateAndCreate(userData);
return user;
}
// Legacy code mixed in
processLegacyUser(legacyData) {
// 200 lines of legacy code
// ...
}
}
// ✅ Good: Legacy code isolated
class UserService {
createUser(userData) {
const user = this.validateAndCreate(userData);
return user;
}
processLegacyUser(legacyData) {
// Delegate to isolated legacy class
return LegacyUserProcessor.process(legacyData);
}
}
// Legacy code in its own module
class LegacyUserProcessor {
static process(legacyData) {
// All legacy code here
// Isolated from the rest of the application
}
}
Advantages:
- Clear separation: Legacy code doesn’t pollute new code
- Easy to find: You know where the legacy code is
- Easy to replace: When you’re ready, you replace the whole module
4. Document behavior, not implementation
Document what the legacy code does, not how it does it (especially if it’s confusing).
/**
* Calculates the total price of an order using legacy logic.
*
* NOTE: This function uses legacy logic that doesn't follow our current standards.
* Do not modify without adding tests first. See: tests/legacy/order-pricing.test.js
*
* @param {Order} order - The order to process
* @returns {number} The calculated total price
*
* Expected behavior:
* - Applies discounts in a specific order (not commutative)
* - Rounds to 2 decimals using legacy method
* - Handles edge cases in a specific way (see tests)
*/
function calculateLegacyPrice(order) {
// Legacy implementation (don't document internal details)
// ...
}
Strategies in practice
Code that works but needs improvement
When you have code that works but that you wouldn’t write today:
Strategy: Don’t touch it unless you need to add features. When you need to modify it, add tests first, then refactor incrementally.
Result: The code keeps working, and gradually gets better.
Complex but functional logic
When you have complex logic that works but is hard to maintain:
Strategy: Use the Strangler Fig strategy. Extract parts into new classes, but keep the legacy logic working.
Result: The system is more maintainable without risk of breaking existing functionality.
Critical code (finance, health)
When you have critical code you can’t afford to break:
Strategy: Add exhaustive tests first, then refactor very gradually.
Result: Safer, more maintainable code, without risk of breaking critical functionality.
Common mistakes with Legacy Code
1. Rewriting everything at once
// ❌ Bad: "I'm going to rewrite this whole module"
// - High risk
// - Long time without new features
// - Chance of introducing bugs
// ✅ Good: Refactor incrementally
// - Low risk
// - New features continue
// - Bugs detected early
2. Ignoring legacy code completely
// ❌ Bad: "I'm never touching that code"
// - Becomes harder to maintain over time
// - Blocks future development
// - Accumulates technical debt
// ✅ Good: Refactor when it makes sense
// - Keeps code maintainable
// - Facilitates future development
// - Reduces technical debt gradually
3. Refactoring without tests
// ❌ Bad: Refactoring legacy code without tests
// - You don't know if you broke something
// - Constant fear
// - Bugs in production
// ✅ Good: Tests first, then refactor
// - You know if you broke something
// - Confidence when refactoring
// - Bugs detected early
The right mindset
Working with legacy code requires a specific mindset:
1. Accept that it exists
Don’t be ashamed of legacy code. It’s part of the process. All code becomes legacy eventually.
2. Value what works
Legacy code works. That has value. Don’t discard it just because it’s not perfect.
3. Improve gradually
You don’t need to fix everything at once. Improve gradually, when it makes sense.
4. Prioritize impact
Refactor code that:
- Is modified frequently
- Causes bugs
- Blocks development
Don’t refactor code that:
- Works perfectly
- Isn’t modified
- Is isolated
My personal perspective
Legacy code is the monster that feeds you. It’s code you’re not proud of, but that generates value. Managing it correctly is crucial.
I’ve worked with legacy code on projects of all sizes. Code that works but that you wouldn’t write today. Code that embarrasses you but that still generates value.
I’ve seen projects that tried to rewrite everything at once, resulting in months without new features and bugs in production. I’ve seen projects that ignored legacy code completely, resulting in code that became harder and harder to maintain.
Don’t try to rewrite everything at once. Don’t ignore legacy code completely. Instead:
- Refactor when it makes sense: When you need to modify code, when it causes problems, when it blocks development
- Add tests first: Before touching legacy code, add tests that verify behavior
- Refactor incrementally: Use the Strangler Fig strategy to replace parts gradually
- Isolate when you can’t refactor: If you can’t refactor, at least isolate the legacy code
Legacy code isn’t your enemy. It’s part of your code. Treat it with respect, improve it when it makes sense, and don’t be ashamed of it. At the end of the day, it’s the code that feeds you, and that has value.