What strategies do you apply to modernizing a product code base? What results do you get with those strategies? This Advisor takes a retrospective look at a past project, both to describe the strategies my colleagues and I used to rearchitect the product and to validate the effectiveness of those strategies with two technical debt assessments via Cutter’s Technical Debt Assessment and Valuation practice. The six strategies we used are presented here. The two assessments are used to evaluate the measured impact on the system from the team’s efforts and compare it to the actual time spent modernizing the code.
This is the story of the DeLorean system, a client’s longtime production setup. (While not the project’s real name, this reference to the time-machine car from the 1985 movie Back to the Future provides an analogy that jumps from today to two points in the past for each Technical Debt Assessment.) This client had successfully developed, evolved, and sustained this system, and its business, for more than a handful of years. This Java Struts Web application embodied the cumulative business and technical experience of the whole company. So far, so good.
A Predictable Story
It’s easy to predict the next part of the story: slowing down, inflexibility, and brittleness. After some point, enough technical debt had settled into the system to cause problems. In this case, the primary causes were multiple inconsistent design approaches over time, significant code duplication, no test automation or manual test plans, and lack of discipline managing technical debt in the code. New features took increasingly longer to complete. Bugs were more difficult to track down and really fix. Changes made to the system needed to be carefully tested by many individuals (manually) and often had nonlocal failures. Getting an accurate estimate of effort for planned changes became difficult. Finally, each new release became an all-hands-on-deck event in which many individuals were prepared to deal with production failures.
All of this made it more difficult to capitalize on business opportunities with both new customer features and partner integrations — the situation was recognized to be affecting the business.
Buy Vs. Rewrite Vs. Rearchitect
Given a consensus view that something must be done, the choices simply boiled down to three options:
- Buy a commercial replacement system, and customize as needed.
- Rewrite from scratch.
- Rearchitect the current production system in place.
When the time came to choose among options, the company had no empirical data to base a decision on, but relied on evaluations and judgment based on the following:
- Suitability of commercial offerings for the business
- Failure rates for big rewrite projects
- Perceived effort to rearchitect the existing system
The management team chose Option 3: rearchitect the application. This choice was based primarily on: (1) the low suitability of commercial offerings and high risk of a rewrite failure and (2) that proceeding with a rearchitecture cleanup would reduce the risks of possibly implementing Options 1 and 2 in the future by providing a cleaner and better-defined platform to move from.
The DeLorean project was explicitly chartered with cleaning up the architecture, removing duplication, improving code quality, building in testing, and improving reliability; in short, to remove technical debt. The team also needed to support new business features and integrating with a customer relationship management (CRM) appliance. The team’s efforts were guided by judgment and experience to balance the progress on all of these goals; no tools were used at the time for measuring the reduction of technical debt or complexity.
DeLorean lasted more than a year, with three to four people at a time working concurrently. Team members subjectively considered “at least half” of their time to have been spent on making improvement to the code and reducing the technical debt. The remaining time focused on developing and integrating new features.
The Rearchitecture Approach
The team’s strategies for reducing technical debt and improving the system evolved over time. The six strategies used on the DeLorean project were as follows:
- “Fix it if you see it’s broken.” This rule gave the team liberty to make the numerous small changes to improve the code by reducing complexity, documenting, refactoring, and adding unit tests. In particular, this meant fixing all bugs in any duplicated copy-and-pasted code. The team was very careful to keep the scope of “fixes” balanced — neither too small nor too large. It was particularly easy to bite off too large a change when each fix would make visible more “broken” code. Both timeboxes and ad-hoc team reviews of scope were used to maintain this balance, serving as metaphorical brake pedals.
- Flexible schedule commitments. The scheduled delivery of new features was not cast in stone, and often the date and features of a production release were altered to support ongoing cleanup activities. The two primary factors that triggered this were: (1) wrapping up refactoring of a significant subsystem or (2) taking the time to clean up and test a region of code newly recognized as high risk for failure.
- Technical management. The manager of the DeLorean team was also a senior technical contributor. This enabled decisions to be made that accurately balanced customer demand/value, technical risk/reward, and schedule. There was little conflict or justification needed when balancing these forces.
- Technically competent and experienced team members. The team had no junior members. The risks associated with modifying untested production code required that each member of the team be able to judge: (1) the likelihood of nonlocal effects, (2) what refactoring and test strategies to apply, and (3) when to call in help from the rest of the team.
- Tracking lines of code removed. Every few days the team would calculate the total lines of code and celebrate when dozens or hundreds of line were removed. Code was removed by refactoring to better design abstractions, through the use of application frameworks, and by deleting copy-and-pasted code.
- Defining unit test and coverage rules for different parts of the code base. New code was written into a new package namespace. This new namespace was the focus of unit testing, held to high craftsmanship standards. Generally, the team code reviewed new modules. A best-effort attempt would be made to refactor legacy code at hand into this new package namespace while adhering to the code, test, and design standards. Any legacy code not moved was given much less attention and focus until the next opportunity for refactoring appeared.
Examples of the standards applied to the new code namespace include: (1) greater than 80% unit test coverage, (2) design modules with DI (dependency injection — a style of composing objects to reduce coupling) into a layered code model, and (3) a preference for clear readable code, a subjective judgment. These standards evolved over time. At the beginning of the project, there were no such rules, and it took some time to understand the testing and refactoring options for the legacy code. Initially, we’d assumed that a single global percent measure of unit test coverage would be desirable, but after creating the new package namespace, the team realized only that code should have testing standards.
Both management and the DeLorean project team maintained a disciplined balance between the priorities of business features and continuing to apply these strategies for reducing technical debt. The day-to-day focus of the team was either blended across those goals or would soon return to a balance after a necessary focus of fixing a bug or releasing an important feature. Where possible, the team applied the strategies for reducing technical debt while implementing new features or fixing bugs.
[For more from the author on Cutter’s Technical Debt Assessment and the DeLorean project, see “Validating Legacy Code: Modernization Strategies Through Technical Debt Assessments.”]