I've inherited a genuinely difficult legacy system with no automated tests, several God objects and methods, around 20 dependencies, a hidden global dependency, and deeply nested conditionals and loops. Many local variables are mutated throughout the main method, so extracting methods would require passing mutable references everywhere.
So far, I've added approval tests to capture the most important behavior and renamed ambiguous variables to make their intent clearer. I can't cover every branch yet because I'm still learning the code and there are too many paths to test efficiently.
I'll be responsible for maintaining this system, and it already has a backlog of bugs, so leaving it untouched isn't realistic. What process or techniques do you use to safely improve code like this without attempting a risky rewrite all at once?
5 Answers
When state is being passed around and mutated everywhere, extracting ordinary methods can make things worse. A useful intermediate step is to create a context or parameter object for the large dependency list, then move the mutable variables into that object as fields. Gradually move related pieces of the long method onto the new class, one loop or responsibility at a time. Once the behavior lives there, you can replace parts of the original God object without passing dozens of references around.
Be careful about deciding between refactoring and rewriting. If the underlying system still has understandable business rules and usable foundations, incremental refactoring is usually safer. If the design is fundamentally incompatible with the requirements, a replacement may be justified, but it needs a clear scope, staged migration, and written agreement about the cost and risks. Either way, set expectations that you’re reducing risk over time rather than fixing every architectural problem immediately.
Start by creating seams around the code you need to change. Characterize one narrow behavior with an approval or integration test, then put wrappers around the global and external dependencies. After that, make small behavior-preserving changes and run the tests after every step. Don’t try to clean up the entire God object just because you found it; improve the parts you actually need to touch.
The strangler approach works well here: define a small responsibility, implement the replacement beside the old code, and route that responsibility to the new component once its behavior is covered. Repeat this gradually until the old implementation has little left to do. It’s less risky than a rewrite and gives you a clean boundary to work toward.
Your approval tests are a strong starting point. Keep adding focused characterization tests around behaviors as you encounter them, and commit very small changes. Rename things, isolate dependencies, introduce state objects, and extract only when the tests give you confidence. The goal isn’t to make the whole codebase beautiful in one pass; it’s to leave each area safer and easier to change than you found it.

That’s exactly the kind of intermediate step I was looking for. Moving the state into a dedicated object seems much safer than trying to untangle every parameter first.