My devs are always refactoring! why? [part-2]
On Wed, 14 Sep 2022, by @lucasdicioccio, 2219 words, 0 code snippets, 5 links, 2images.
This article is the second in a series of articles on refactoring.
From a non-developer viewpoint, it may be hard to connect with developers who are always refactoring. I wrote this article to fill a gap I noticed during multiple discussions with non-technical stakeholders: some stakeholders have the feeling that tech teams are “always refactorin’”. Depending on the type of relationships between developers and other roles, the question whether to refactor or not can become a point of acute tension.
Refactoring to support #project-scope changes
Let’s recollect what we introduced in the previous article about project scopes and the complementary toy-application. As software projects grow, we accumulate code to enable features.
When a new feature is required, we are faced with an easy decision: pause in-progress work and prioritize on new features or continue on what is in progress. Conversely, when a new technical or process requirement pops-up, we are faced with more difficult decisions: we need to revisit everything in scope that has been shipped already. A decision to implement or postpone is required for each individual feature already shipped and in scope. Typically, one can live with some gaps for some time (e.g., missing tests, hardcoded parameters, outdated libraries, manual approval in an otherwise automated process, un-even edge-case coverages for two flavors of a same system). However, at some point the infamous technical debt weighs too much and a feature can be considered unfinished. In short, as requirements pile-up there is some risk that your effective coverage reduces even though the amount of delivered tasks increases, an effect that I refer to as a coverage cave-in effect.
Refactoring is an attempt at mitigating cave-ins for upcoming requirements. The mitigation can be either anticipated or reacted-upon. If you anticipated, you do not observe the “cave-in” gap but the totalled amount of refactoring work incurs some delay. If the refactoring work is reactive you get to do the same amount of work anyway, except that you do it while your effective coverage is reduced. Overall, engineers tend to like refactors: they will recognize the risks of cave-ins and will often express the need to “refactor first”. As refactor are pushed in the future engineers keep discussing the need to refactoring. When the refactoring starts, it takes longer than expected and leaves even more extra work. Overall, I have witnessed, and I can see a number of scenarios in which product managers grow the feeling that engineers are always refactoring.
Refactoring
Refactoring is a technical task and a technical challenge: change how a component operates without affecting the external behavior. People obsessed with “customer value” raise eyebrows because a refactor brings “no value” to a product. Pedantic engineers will note that business value is not only customer value. Indeed, engineers do not refactor for the desire of challenges: engineers refactor to reach a more favorable state than from where they started.
Examples of goals for refactors are:
- uniformize some idioms in a code-base so that (e.g., when merging two implementations from two different maturity levels a your product)
- keep an acceptable pain-level for people who need to maintain or operate the system
- prepare for planned ulterior scope changes (e.g., in order to internationalize a service)
In the first example, the value resides in paying-up some “tech debt”. In the second example, the value is pairwise: liberate resources for higher-value tasks and improve the satisfaction of team members (employee churn is an active threat to teams’ success). Finally, the third example is merely time-shifting future work into the present, without changing much the actual customer value.
Among engineers, some purely-technical tasks like “changing the logging format” may not be recognized as “a honest to god refactor”. Somehow, as far as our discussion is concerned, and as far as engineer-product communication goes, the decisive characteristics we care about is the absence of customer-value. The “refactoring” label merely is a shorthand for how to classify this task: it takes work but if you are not an engineer you need not know how the sausage is made.
Misunderstanding the goal of a refactoring task may lead to some breach of trust between product and engineering ⚠️. The main catch is: developers’ happiness and morale are part of their productivity and you have an incentive and some moral obligation to create virtuous cycles rather than self-defeating feedback loops.
I believe my team is always refactoring what should I do?
Now that background is setup, let start the real discussion. You are in a frustrating situation where the team delivery feels slow, and engineers seem to do refactoring over refactoring.
First, is it really true?
In all companies, you need to settle on some acceptable amount of technical work, which includes refactors. In a sense, it is the “cost of doing business”. A fork of accepted technical work varies from 20% to 50% of technical work . The proportion you observe should probably fluctuate within this range and could deviate more depending on the erratic aspects of companies’ timelines.
However, a team deviating for too long may be a reason for concern: too little technical work probably means you are post-poning tasks you should have done already, too much time spent on technical work may mean your team lacks purposeful tasks or maybe they are circling and figuring things out (in the latter case, they need hindsights from senior engineers/architects).
As illustrated in the above picture, if you plot the fraction of work spent along time, you should expect some team to stay in the 20-50% band. In this example, the green-plain line is okay, the two dashed-lines probably deserve some investigation. The 20%-50% bracket is more a rule of thumb speaking from experience (personal, colleagues, also a recent Twitter poll I made in preparation of this article) than anything.
One caveat here is that you need some fair assessment of how much engineer time is taken by refactorings. From experience, I’ve seen that it may be challenging for tech leaders to provide such a fair assessment. Without turning this post into some advertisment, my current company provides a service to solve this question among many other questions.
Now, assuming that you have legitimate concerns regarding some refactoring work, let’s discuss the timing of refactoring. Then we’ll discuss avenues to challenge and push back a team member who wants to refactor something when you think there are better things to do.
The timing of the refactoring is key
Project planning, sprint sessions, and similar corporate ceremonies are venues where teams discuss the need for refactorings. Alas, no-one is encitivized to tell the truth in such ceremonies: time or complexity estimates always are fudged and a complex meta-game between makers and askers happens around roadmaps. Such ceremonies deserve better. If your teams are always refactoring, you have likely witness a heated discussion when a developer brings up a refactoring task. In this situation, it is a good idea to keep in mind that developers will bring up refactoring tasks for a reason, and in general to avoid some functional cave-in.
Refactorings exist mostly to prevent some form of looming or occurred functional cave-in. Thus you cannot get a lot of information by challenging “why” people refactoring. A more interesting characteristics for a refactor is the question of “when”. A refactor can be preliminary work, wrap-up work, or intermediary work.
-
As preliminary work. Most features benefit from chunking into many small tasks. Preliminary refactors (e.g., moving all functions into some common umbrella module) are easy to plan and scope ahead of time. Consider these as stepping stones reducing the risk of a particular delivery.
-
As wrap-up work. One may consider a feature unfinished from a technical standpoint while it is already delivering on a functional standpoint. It is common to deploy some Proof of Concept early to test a feature. These Proof of Concept often are “too large”. The hindsight gained from running a system is light shedding. Engineers will find flaws and limitations in their system that would have been better handled in other ways. I would advise budgetting some time to perform such wrap-up as engineers still have a lot of working knowledge of the code. On the opposite, moving onto to new features in a haste with no time to wrap-up is a longer-term risk.
-
As intermediary work. The fractal nature of projects’ scopes and the erratic nature of scope discovery ensure that hiccups occur. Some severe obstacle may pop-up while implementing some feature (e.g., a third-party API exhibits severe rate-limitations on an endpoint you used to rely on and you need to pass some cache in a whole slice of code, adapt tests and so on). You may not be aware of most of these refactors as small refactors occur as part of the normal flow of development. Incredible delays may occur when the larger system or infrastructure require changes. A way to rationalize these intermediary refactors is to consider them preliminary or wrap-up of some subtask.
Thus, in the bad cases, for one single feature, team members may argue three times in favour of refactoring. Given that many features are discussed in ceremonies, engineers may come across as people who are always refactoring.
At this point, I mostly gave arguments in favor of refactor: they tend to go in the right direction. The real issue of refactoring is that they take up time from other concerns. Thus, if you feel like your team is always refactoring, the main problem is the one of arbitraging against other topics. You have two main ways to reduce the “refactoring-tax”: one one hand you can push-back on refactoring, on the other hand you can reduce the prevalence of refactoring with some prevention.
Preventing refactorings
In an ideal world, you never have debates about refactoring because the team orchestrate development with a perfect context and good understanding of the situational challenges of the day. As a leader you need to be candidly honest about upcoming tasks and deadlines. Having an idea for a feature it’s not the same as having a customer request and it’s not the same as having twenty customer requests. Needing something for next week is not the same as needing it for next month nor is the same as potentially needing it.
Summarily, engineers need to gauge the amount of uncertainty you have with a feature. Of critical importance are the following characteristics:
- whether the feature will be in active use or in potential use
- whether a requirement is definite or a sketch of an idea
- what is the freedom for digressing
- what are the business gains and risks
Having this information at hand helps engineers understand how to shape the delivery of the code and associated “non-functional requirements”. For instance, if a feature requires some good amount of data-crunching, a preliminary data-exploration phase is welcome to help understand where edge cases lurk. If the task requires a system and not just some code, extra monitoring, sometimes ad-hoc, will have to be built. If engineers understand the business opportunities and risks associated with a feature, they’ll be in better place to gauge how-much these “extras” are required.
Pushing back on refactorings
Having in mind that refactorings are mostly-positive for the health of a software. Senior engineers on a #project may resent push backs and even identify such push-backs as a threat to the qualify of the system. Therefore, a goal to keep in mind is not to say “no” to a refactor. Rather, a productive conversation seeks a good #tradeoff for every party.
There are a few avaialble directions to tackle a “refactor-or-not” discussion.
-
Bluff it out. Bluffing is a good way to lose trust from your colleagues. You’ll come across someone who is dishonest if you bluff without success to celebrate aside. Some bluffing techniques are along delaying to improve the understanding of the system you want to refactor, or some planned work that will make the to-be-refactored system obsolete.
-
Clarify whether the refactoring is a pure-technical change or whether the refactoring enables new features. Sometimes refactoring is an enabler for pushing more feature (e.g., when you have data to filter/sort by a set of fixed criteria and suddenly it’s better to change some querying-scheme such as supporting “arbitrary” and likely-requested future criterias). This is the best situation as you’ll learn better what are/are-not low-hanging fruit features.
-
Characterize qualitatively and if possible quantitatively the friction induced by the absence of refactoring. You should be able to tell if the refactoring specifically adress the pain point. You should understand whether the gain is on the long-term or on the short-term. Maybe a piece of code is garbage, but is not touched very often leading to “one horrible day every quarter”, which may be acceptable pain.
-
Chunk. Like most software endeavours, it’s better to split refactoring in consecutive well-defined chunks. Often, one can split and time-box the refactoring effort: for instance, do some code re-organization in a first part then apply fundamental changes in a second part. If you frame a long-refactor as a low-risk migration tasks spread over weeks you also train your team for more dangerous and longer-to-rollout migrations. The risk here is that being low-ROI the task never finishes.
In summary, you do not have a large number of options to push back on refactorings. Bringing more people on the team can help with a punctual increase in work. Oftentimes you’ll have a better luck ensuring the overall team momentum is maintained.
Conclusion
Cautionary tale: challenging whether a refactoring is necessary or not is a sure-fire way to sow dissent and lose momentum. If you’ve read my long article on successful tactical projects, you’ll know how much I care about momentum.
In general you should pay attention to functional cave-in and anticipate when your project will go sideways. Sometimes a business-requirement may ask for technical changes throughout the project (for instance, data-modeling changes incur verification on already-shipped code).
Allocate a fair amount of work onto technical tasks. Consider that purely-technical work is part of the cost of doing business. If engineers keep bringing-up refactorings over refactoring there are things you can do to push-back without too much trouble. Ask clarification around the functional benefits of the change, characterize with the team what pain-points the refactoring addresses, consider chunking the refactoring in a few steps to allievate only the most pressing pain-point.
If nothing works, consider the help from senior engineers (architects, lead-developers) because your team may lack from technical direction. Of course, if egos prevent your team members from seeking external consulting, you will have a hard time convincing them to change their way of doing thing.