Bottom-Up Design: The Best Way to Refactor Messed-Up Code
Throughout my career in software development, I've encountered a recurring challenge: inheriting codebases that are messy, disorganized, and fragile. The common impulse in such situations is to suggest a full rewrite. While this might sound appealing at first, it is usually the wrong move.
The smarter, more sustainable approach is to introduce design to the existing mess. Not by imposing a grand vision from above, but by letting the code itself guide you. This is what I call bottom-up design, and it is, in my opinion, the best way to refactor a problematic codebase.
Why Rewriting Often Fails
Rewriting an existing system from scratch is a massive undertaking. You lose all the small decisions and hidden logic that exist in the current system. Even worse, you now have to develop and maintain two systems in parallel: the old one that’s still in use, and the new one trying to catch up.
As I discussed in my article Software design: underrated and not practiced, skipping design from the beginning often leads to "programming by patching," a messy cycle where bug fixes introduce new bugs, and the code becomes increasingly unmanageable.
What Is Bottom-Up Design?
Bottom-up design is the process of extracting structure and order from an existing codebase. Instead of rewriting everything, you:
-
Start small, focusing on specific units of functionality.
-
Refactor them into clearer, more isolated modules.
-
Introduce meaningful abstractions and interfaces.
-
Use good naming and documentation to make the code self-explanatory.
Bit by bit, design emerges. The system becomes easier to understand, test, and extend.
How It Works in Practice
Here’s a practical bottom-up path:
-
Find the boundaries: Identify natural seams in the code. Functions that do too much, classes that contain unrelated logic.
-
Extract responsibilities: Split code into smaller, focused components.
-
Name and document: Express intent through names and comments (see The Importance of Code Comments and Documentation).
-
Define interfaces: Formalize how parts of the system interact.
-
Iterate: Each small improvement compounds, gradually shaping a robust design.
This approach supports continuous delivery. You don’t need to freeze development. You’re improving the system as you work.
Bottom-Up vs Top-Down
Top-down design is excellent when you're starting from a clean slate. But when dealing with legacy code, top-down plans often fail to account for the messy reality. Bottom-up design meets the system where it is and builds upward from there.
The Real Benefit: Clarity Through Design
By applying bottom-up design, you avoid the cost and risk of a rewrite while achieving many of the same benefits: better structure, improved readability, easier maintenance, and a cleaner architecture.
In fact, I believe this process reveals the design that should have been there from the beginning.
So next time you find yourself deep in a legacy code swamp, resist the temptation to rewrite. Try introducing design from the bottom up. You’ll not only improve the code, you’ll gain deeper insight into the system itself.
And that’s the essence of real software engineering!
Author: Dr. Sotiris Fanou