Discover Azul's high-performance Java platform providing faster speed, startup, & efficiency without code changes
Blog chevron_right Security

Your Codebase is a Cluttered Garage Full of Unused and Dead Code

A version of this article originally published on DZone.

Over the last few months, I’ve been working on a feature that helps development teams produce less code. Everything we read is about working with new frameworks, new tools, and new techniques – but one thing many of us ignore is improving velocity by simply getting rid of things we no longer need. If you’re a senior developer helping new teammates, consider the work it takes to onboard new members and for them to learn the code and business. Each time they change something, they scroll past methods, their IDEs help manage navigate through the files and methods, but is it all really needed? As I’ve talked to teams about the idea that we hoard dead code, I’ve heard comments like these:

Unused Code: We scroll past it so it's a minor issue, but yeah we do it a lot.
Unused code: I could clean that up, but it's a priority issue and we don't have time.

What if Java developers had an easier way to identify dead code for removal – a way where we could prioritize code cleanup during our sprints to reduce technical debt without taking time away from business needs to add features?

Code removal is complex and generally takes a back seat to new features. Over time code becomes unused as teams refactor without removal: commenting an annotation, changing a path, or moving functionality. Most senior engineers would have to allocate time in their sprints to find what to remove: evaluating missing log statements or reviewing code with static analyzers. Both are problematic from a time perspective, so many teams just leave it in the code repository, active but dead: a problem for a future team lead or delayed util the next big rewrite. The JVM however, has an overlooked capability to identify dead code and simplify the prioritization problem. By re-purposing the bytecode interpreter, the JVM can identify when a method is first called per execution. When tracked in a central location, these logs produce a treasure map you can follow to reduce dead code, reducing the overall cognitive burden and improving team velocity. If a method hasn’t run in a year, you can probably remove it. Team leads can then take classes and methods that haven’t executed and remove that code either at one time or over the course of several sprints.

Why remove dead code at all? For many groups, updating libraries and major Java versions requires touching a lot of code. Between Java 8 and Java 17, the XML libraries were deprecated and removed – as you port your application, do you really still use all that XML processing? Instead of touching the code and all associated unit tests, what if you could get rid of that code and remove the test? If the code doesn’t run, team members shouldn’t spend hours changing the code and updating tests to pass: removing the dead code is faster and reduces the mental complexity of figuring that code out.  Similar situations arise from updates to major frameworks like Spring, iText, and so on.

The problem of cluttered and unused code also affects teams working on decomposing a monolith or re-architecting for cloud. Without a full measurement of what code is still used, teams end up breaking out huge microservices that are difficult to manage because they include many unnecessary pieces brought out of the monolith. Instead of producing the desired streamlined suite of microservices, these re-architecture projects take longer, cost more, and feel like they need to be rewritten right away because the clutter the team was trying to avoid was never removed. Difficulties stick with the project until teams can decrease the maintenance burden: removing unnecessary code is a rapid way to decrease that burden.

The benefits of tracking dead code

The distinguishing benefit of tracking live vs. dead code from the JVM is that teams can gather data from production applications without impacting performance. The JVM knows when a method is first called and logging it doesn’t add any measurable overhead. This way teams that aren’t sure about the robustness of their test environments can rely on the result. A similar experience exists for projects that have had different levels of test-driven development over their lifetime. Changing a tiny amount of code could result in several hours of test refactoring to make tests pass and get that green bar. These unit tests give an inaccurate view of the code’s importance: if the fully tested code never runs in production environments, then time spent making tests pass is time wasted. Making those unit tests pass gives a false sense of accomplishment and simply removing the code will have a better impact on overall project health and team happiness.

The best way of identifying dead code for removal is to passively track what code actually runs. Instead of figuring it out manually or taking time from sprints, tune your JVM to record the first invocation of each method. Later on during sprints or standard work, run a script to compare your code against the list to see what classes and methods never ran. While the team works to build new features and handle normal development, start removing code that never ran. Perform your standard tests – if tests fail, look into removing or changing the test as well because it was just testing dead code.

By removing this dead code over time, teams will have less baggage, less clutter, less mental complexity to sift through as they work on code. If you’ve been working on a project for a long time or just joined a team and your business is pressuring you to go faster, consider finally letting go of unnecessary code.

Clean Your Code

Learn more about Azul Code Inventory today.