Finishing what you start... One way or another

12/06/2019

In Software Delivery, as in life, closure is important.

In this instance, I’m not talking about the giant projects that never end, although they deserve a special place all of their own. Instead, I’m talking about the code that is delivered for the feature or project that later gets canned.

Depending on the length of time spent working on this abandoned project or feature - hopefully not too long in this day and age - you’ll be left with a certain amount of “cruft” sitting in the codebase. New classes, modules, APIs or entire project files that are no longer required. In the old days of big bang development, abandoning this work was simple enough. You’d delete the feature branch that all this stuff was building up in, and given you hadn’t released anything yet, there wouldn’t even be a need for QA effort to check everything was fine.

Thankfully, we don’t really build software like this any longer. Instead, we favour a more incremental approach, shipping one feature at a time or continually integrating code behind feature toggles that are only switched on once everything is good to go. Yet for various reasons, sometimes even these approaches lead to products that are fully developed and yet abandoned at what is nearly the final moment. Business conditions are unpredictable, and its often items outside the control of the delivery team that see such initiatives canned at the final moment.

Take for instance a large project at a former workplace, which was an integration job with an external loyalty program provider. Our team progressed through large portions of the project’s scope; designing new products branded for the provider, tested and released to production (toggled off of course). We had a new loyalty program module good to go, verified against some mock transactions to ensure points would be calculated correctly. We’d also gone ahead and made changes to various systems to ensure that customers’ loyalty details would be captured at point of sale. As good, responsible software developers, we made sure that this all worked but then hid it all from view until the Product Manager was ready to hit the ‘GO’ button.

Unfortunately, this day never came. Late in the piece, it was announced that the loyalty provider and our company were unable to settle on commercial terms for their planned market release. Now you may question the wisdom of starting work on this initiative before contracts have been agreed and signed by the relevant parties, but in any case that wasn’t the domain of the product or delivery team; rather, it was a decision made much higher up. Then, in an effort to minimise sunk costs, the team was moved onto another piece of work, given little to no time to remediate what was now going to become software cruft. While not throwing good money after bad is admirable, and something that needs to happen more often in project circles from my experience, doing so in such a sudden manner late in the piece has unforseen consequences.

The software that has been shipped to this point, while not necessarily “live” for a user, is still deployed. It’s still adding time to builds, to the running of tests, or potentially even the startup time of your services, assuming that’s important. Furthermore, there are other problems leaving this code in place. Developers working on the codebase see feature flags and aren’t necessarily sure if they’re needed or not. They hesitate to refactor because there are usages in these code paths that they’re not entirely familiar with. “Maybe it’s needed, so I’ll leave it in place, just in case…” becomes a common phrase, and unit tests that break just need to be fixed… I mean, that’s just good practice, right? This is all not to mention the test cases written for the features that we one day might turn on, or the wasteful documentation or domain models that might exist for something that is never actually going to come to fruition - and we’ve talked about that before.

Recently, I found myself in a similar situation again. We’d done a solid three months worth of work to integrate with a Telecommunications provider, only for their company to undergo a gigantic restructure and can our integration - even though we were practically at the point of go-live. The services affected by the initiative had substantial changes made to them, and we’d essentially go on and carry that debt while delivering the next hot item in a bid to recoup the effort lost on the Telco. Soon though, it would become obvious that the code we’d added was weighing our codebase down, hindering our efforts to deliver further changes rapidly. To make matters worse, we’d introduced a fair amount of duplication in an effort to hit an agreed date, and this duplication was now killing us as we worked on a similar integration for another company.

The difference in this scenario though was that we had the autonomy as a team to sit down with our Product Owner and agree on a path to deliver the new integration and remove the work for the abandoned partner along the way, taking the best bits of both and removing the duplication we’d added in. This meant a bit of give and take on both sides - developers agreed to initial further duplication to hit a key milestone, while the PO agreed to a period of consolidation thereafter where “payback” was done on the debt, meaning new features would be delayed to ensure there’d be a better foundation to build them upon. The trust that exists here is key, and made what would otherwise be a difficult project-level negotiation a relatively straightforward exercise. Not everyone will understand the ins and outs of software architecture or API design, but any reasonable person wearing the hat of Product Owner can see the waste inherent in having three different ways to do the same thing.

While we’ll soon have a large commit consisting of a lot of deleted lines of code, happily the other story has a happy ending too - albeit one that took a substantially longer time at a higher accrued cost. Eventually, through a combination of “support” work and gradual refactorings, another team was able to remove the legacy that my own team had left behind on the abandoned loyalty project. I understand a few little things remain here and there, but the large majority of excess fat has been trimmed. It’s unfortunate that the feedback loop took so long to be closed, and that a good amount of that work had to be done by ‘stealth’ - but at least the loop is closed nonetheless. What would be even better is if they had some way of demonstrating the accrued cost along the way; the opportunity cost of the work they and their peer teams could have done had it not been for the time they were slowed down by unused code they’re almost sure could be deleted, but never was.

This orphaned and abandoned code, much like projects we realise are going nowhere, should be killed off as early as possible. Failure to do so adds overhead and complexity to the codebase and the future development of the whole delivery team. Really, if we’re going to apply YAGNI to the early design choices that we make, we need to be equally willing to apply it to the stuff we’ve left behind.

And if nothing else, do it for yourself - or for whoever the people were who originally wrote that code and were then forced to leave it to rot - and give yourself some much-needed closure.