Creating a Web Project: Refactoring
See how to approach refactoring as a strategic investment in your codebase. Learn best practices, when not to refactor, and how to use automated tools and metrics to guide your efforts.
Join the DZone community and get the full member experience.
Join For FreeIn the previous article, we spoke about hunting bugs. But a time will come when your hunters’ ‘trophy collection’ grows to a scary size. More and more ‘targets’ will be coming as if it were a zombie apocalypse. There will be red lamps flashing across your metrics dashboard. If (or rather when) that happens, it would mean it’s time for a bigger change than just routine debugging and streamlining your codebase. Today, I am going to talk you into refactoring, which is basically digging into and altering the very foundations of your project. Don’t take anything for granted, check everything yourself and think twice before diving into this work. It’s just as easy as swapping an engine in a flying airplane. But that’s exactly what you will have to do. Let’s get started!
Monsters Beneath the Surface
In the troubleshooting guide, I already mentioned how important it was to create and closely watch the metrics of your project. System metrics — like response time, memory consumption, etc. — and product metrics — like user acquisitions and engagement — are equally important (you watch and log your metrics, right?). It may happen that the problems you see with these instruments keep mounting up. Solving them consumes more and more time and resources, and you become short of both for growth and development. This is the worst symptom that your project needs refactoring. Ideally, you should be alert before skyfall gets at your doorstep. But for a lot of people, "If it ain't broke, don't fix it" is a motto for stability. Well, it doesn’t really work that way. You will actually save more if you start refactoring your project before real trouble hits you.
The main point to keep in mind is that refactoring is an investment, not an expense. Companies that ignore it often face a snowball effect of problems: bug fixes take longer, teams lose motivation, and users lose trust in the product. True enough, it takes time to analyze, modify, rewrite, and test your code. But postponing changes can cost you even more. As technical debt accumulates, developing new features becomes increasingly difficult, and the costs of maintaining an aging system grow. Over time, even a tiny bug in poorly maintained code can lead to huge losses, sometimes far exceeding the investment needed to improve it.
Another popular misconception is that refactoring is something like mature age skincare: young projects don’t need it. First, even young projects can suffer from the acne of technical debt, especially if they were developed in a hurry or without proper architectural planning. Second, refactoring is not related to the age of your code. Just think of the tools and libraries that your code employs. Trace their origin right to the roots and you will be surprised if not shocked. According to statistics based on over 7 million open-source projects, around 70% of open-source tools and libraries that modern software relies on are either unsupported or in poor condition. And the situation in closed-source software is unlikely to be much better. This is the clearest illustration of what happens when the "If it works, don’t touch it" mindset is taken to the extreme.
Refactoring isn’t just about fixing past mistakes, it's also a way to build a better future. It helps adapt a system to new requirements, improve performance, and make maintenance easier.
Important "Don’ts" Before You Start
So far, I may have sounded like a refactoring ambassador without any second thought. Of course, that is not true. The first and most important ‘don’t’ is: don’t do refactoring if it brings more harm than good. Be careful about refactoring if one or more points in this checklist are true:
- The project is nearing completion or is already scheduled to transition to a new system. If you plan to replace an outdated CRM system in six months, it makes more sense to focus on building the new one rather than trying to "patch up" something that will soon become obsolete.
- Users and stakeholders are fully satisfied with the current functionality. No future changes are planned, so there’s no need to work with outdated code.
- The system is isolated and not integrated with other systems, meaning it’s not affected by external changes or errors.
- The risks of making changes outweigh the potential benefits.
If your case has passed this brief test, there are a couple more refactoring taboos you should be aware of.
There is always a rather idealistic temptation to rewrite everything from scratch to get things right, tidy, and perfect. Well, first, there is no such thing as perfection. Rewriting almost always leads to losing critical details, missing deadlines, and introducing new, unexpected bugs. And let’s be honest, such ambitious projects rarely get completed. Your legacy system is not just code. It’s accumulated experience, bug fixes, and an understanding of real user needs. Refactoring allows you to improve the system without losing its core functionality. This evolutionary approach enables gradual improvements without breaking the foundation or increasing risks.
Another temptation is to kill two birds with one stone and combine refactoring with adding new features. Believe me, that is a no-no. First, refactoring and business tasks are completely different targets. It is simply not possible to keep an equal focus on both at the same time. At best, you will only hit one of them. But in most cases, you are going to miss both. Also, you will multiply confusion, delays, and you will have even more bugs as business logic gets entangled with refactored code. So please keep refactoring and business tasks separate. This way, you can track how much time is spent on business features versus technical improvements. And your code reviewers will really, really appreciate it.
Step #1: Be Discreet, Think About Code Quality
Do not rush to refactor every piece of old code indiscriminately. Typically, a project is developed by different people with varying levels of experience over time, and in some cases, what appears to be "bad" code from your perspective may not actually be that bad. Also, do not try to cover the entire project at once: break refactoring into small, independent tasks that can be completed sequentially. It is best to start with the most problematic parts of the code (so-called hot spots) that most frequently cause failures or hinder development. Remember the Pareto principle? It works pretty well here: refactoring 20% of simple code will give you an 80% improvement in the codebase.
There are many tools capable of automatically assessing the quality of the codebase. For example, SonarQube is an open-source platform for continuous analysis and measurement of code quality. It supports all popular programming languages, from Python, JS, and PHP to Swift, Java, and C. This project has excellent documentation and can even be used with popular IDEs.
Another popular solution is Qodana from JetBrains. It also supports all major programming languages and integrates with various development environments and CI/CD pipelines. Qodana can check dependency license compatibility according to specified policies and aggregate reports from other analyzers. Overall, it is a powerful tool, though the free version has limited functionality. However, the paid version costs only $5 per month per developer in the Ultimate edition, making it a very reasonable choice, especially if you are using JetBrains IDEs.
There are also many simpler alternatives. For example, you can use open-source static code analyzers: for PHP, a good choice would be PHPStan or Psalm; for Python, pylint or flake8; and for Golang, in addition to the built-in solution, staticcheck could be used. All these solutions are configured via configuration files, allowing you to scan only specific parts of a project rather than the entire codebase. This way, you can gradually identify problematic areas for refactoring, step by step. Tools such as PHPMD (PHP Mess Detector) for PHP or PMD (Programming Mistake Detector) for Java, JavaScript, Kotlin, Swift, and other languages can also highlight the need for refactoring. These tools analyze code to identify excessive complexity, duplication, outdated constructs, and potential errors. They help pinpoint problem areas that require attention and provide recommendations for improvement.
Determine the general style and rules for writing new code in advance. Try to follow widely accepted standards. For example, in PHP, apply PSR; in Go, adhere to the standard gofmt and golangci-lint; and in Python, follow PEP8. Be sure to discuss the chosen standards with the team in advance so that everyone has a unified understanding. To ensure that the new code always complies with these standards, add linters to the build process. For example, configure automatic checks before pushing or PR. Git hooks (if you use Git) can help with this. For instance, you can create a pre-commit hook that automatically runs static analysis before committing changes and blocks the commit if something goes wrong.
The new code should already comply with the new rules to avoid increasing the amount of code that requires refactoring in the future. Once the new code meets the standards, gradually extend the checks to small old sections of the project. Try to start with the least complex parts. Do not hesitate to use automated refactoring tools like PHPRector, Rope, or gopatch (such a tool can be easily found for any programming language). These tools will easily help bring the code to the expected state, you just need to configure them correctly.
Step #2: Testing and Refactoring Tests
In addition to code quality, it is crucial to monitor test coverage in your project. Actually, a lack of tests is also an indication that refactoring and addressing technical debt are necessary. To assess test coverage, you can use tools such as php-code-coverage for PHP, Coverage.py for Python, or Istanbul Code Coverage for JavaScript. Similar tools exist for every popular language. These tools generate reports showing which parts of the code are already covered by tests and which still require attention, which will certainly help improve the quality of your project's testing. Notably, SonarQube and Qodana can also analyze code test coverage. In short, choose the most suitable tool for your needs; the selection is really extensive.
Please mind that there is a difference between tests that push us toward the very idea of refactoring and tests that should accompany its implementation. We are trying to improve code that has existed for quite some time. Therefore, it is most likely deeply embedded in the business logic, and modifications can break the project in the most unexpected places. That is why good testing is indispensable here. Of course, during refactoring, some old tests may break or become obsolete due to significant functional changes, but high-level and mid-level tests will provide safety. Nevertheless, do not forget to refactor the tests themselves; they are also an important part of your project.
Step #3: Tiny Bits That Make a Huge Difference
Remember how I told you about how vulnerable you may be to third-party tools and libraries that become obsolete? Refactoring is a perfect time to address that issue. In a modern environment, being dependent on ready-made libraries and frameworks is inevitable. You should actually minimize the number of ‘reinvented wheels’ in your project by choosing proven solutions developed and approved by the community. But of course you should choose them wisely. Before integrating a solution, make sure that the library is actively maintained, its license suits your project, and its version is locked in the dependency manager. By the way, it is a good idea to keep forks of these libraries: this way, if something happens to the original versions, your project will not suffer. Also, regularly update dependencies, but test the changes beforehand to avoid unexpected problems.
Speaking of third-party solutions, it is always a good idea to update them. This applies not only to libraries but also to servers, databases, and operating systems. Outdated software can be vulnerable or unstable. Before updating, be sure to test it in an isolated environment.
Add checks for vulnerabilities and secrets in the code. There are many tools for this, such as Trivy, Snyk, or Bandit. These checks can also be integrated into the CI/CD pipeline to detect issues early. If secrets are stored in the code, that is definitely something that should be refactored as soon as possible. Where should they be stored? Check solutions like HashiCorp Vault, AWS Secrets Manager, or GitHub secrets.
Last but definitely not least, do not forget to update old documentation! Yes, documentation is also a part of your project and should never be neglected. If refactoring only breaks the documentation, its benefits are significantly reduced: by fixing one problem, you are merely creating another for yourself in the future.
Step #4: Product Is King
Your codebase is important, but its value is always determined by the problems it solves. Code is a tool for achieving project goals, and we must not forget about end users and their needs. So it is crucial to continue keeping an eye on your metrics as your refactoring progresses. Metrics were the first to signal there were problems, but now the focus is different, although the metrics themselves remain mostly the same. During refactoring, it is essential to track both system metrics (such as system response time, resource usage, and error count) and product metrics (such as conversions and retention rates). These data points will help you understand whether the system is actually improving from the user’s perspective and where bottlenecks still remain.
If you ignored the advice from both this and the previous chapter and still do not have any metrics, at least set up system data collection before making any changes and wait for a sufficient amount of data to accumulate.
Conclusion
I hope I made myself clear that refactoring is not just change for the sake of change, but a strategic tool that helps keep your project alive, efficient, and maintainable. If, of course, you had any doubts about that before. Fortunately, there are now a vast number of tools and methods for analyzing and automatically improving code, allowing developers to do it in a quick and (almost!) painless way. I have tried to mention some of these tools, but it is impossible to cover them all.
Progress does not stand still, and new, smarter AI-driven refactoring solutions based on LLMs are already being actively developed and implemented. Perhaps in the future, all we will need to do is press a single button to make our code cleaner and more understandable. But until that button exists, don’t forget to refactor manually. Without regular code improvements, your system risks becoming as much of a cumbersome legacy as a CRT monitor: new features will take longer to implement, fixing bugs will become increasingly difficult, and developers will lose motivation before they even start working. May refactoring be with you, and may it bring you only benefits!
Opinions expressed by DZone contributors are their own.
Comments