Legacy Code Isn't the Enemy — Ignorance Is
Every few years I work with a team that's convinced their codebase is uniquely terrible. The system is old, poorly documented, full of edge cases nobody remembers the reason for. The instinct is always the same: rewrite it. Start fresh. Do it right this time.
I've seen this play out enough times to have a strong opinion: the rewrite impulse is almost always wrong, and the real problem is almost never the code itself. It's the gap in understanding what that code is actually doing.
Legacy code carries institutional knowledge
That tangled conditional in the payment processing module? It probably handles an edge case that burned someone badly in 2009. The weird date arithmetic in the scheduling engine? Some client's fiscal calendar doesn't align with the Gregorian calendar in the way you'd expect.
When you rewrite without understanding, you don't eliminate those cases — you just stop handling them correctly. The business finds out six months after launch when a client calls about a billing discrepancy that the "old system" somehow handled fine.
The reason legacy code is hard to understand is not because the original developers were bad at their jobs. It's because the software accumulated real-world complexity over time, and that complexity is legitimate.
The right first step is always comprehension
Before touching a legacy system, I spend time understanding it as-is. Not by reading the code top-to-bottom — that rarely works for large systems — but by asking the right questions:
What are the actual inputs and outputs? Where does data enter the system, and what does it look like when it leaves? What are the failure modes that the team has already encountered and patched? Which parts of the system have the highest rate of change, and which have been untouched for five years?
This work is slower and less glamorous than starting a new Spring Boot project with clean architecture. But it is the most important thing you can do before proposing any modernization strategy.
Strangler Fig, not big bang
When it's time to modernize, the strangler fig pattern is almost always the right approach. You build new functionality alongside the old system rather than replacing it wholesale. The new code grows; the old code shrinks. At no point is the business running on an untested rewrite.
// The strangler fig approach in practice: // 1. Identify one bounded module to extract // 2. Build the replacement with full test coverage // 3. Route traffic to the new module behind a feature flag // 4. Monitor, validate, then retire the old code // 5. Repeat — never do the whole system at once @Service public class PaymentRouter { public PaymentResult process(Payment p) { if (featureFlags.isEnabled("new-payment-engine")) { return newPaymentService.process(p); } return legacyPaymentService.process(p); } }
Tests are how you understand and protect legacy systems
The best investment in any legacy modernization effort is a characterization test suite. Before changing anything, write tests that document what the system currently does — not what you think it should do. These tests become your safety net for every subsequent change.
Teams that skip this step and go straight to refactoring almost always break something they didn't know existed. Teams that build characterization tests first can move quickly because they have confidence the system still behaves correctly after each change.
The real enemy
Legacy code that's well-understood is manageable. Legacy code that nobody understands is dangerous — but the solution is to build that understanding, not to throw the code away and hope the new version somehow avoids the same fate.
Software that's been running in production for a decade has survived contact with reality in a way that a brand-new rewrite has not. Respect that. Start there.