DEV Community


Prefactoring: A pragmatic approach to tech debt

alexfoxgill profile image Alex Gill Updated on ・4 min read

prefactor (verb): a portmanteau of ‘preparatory refactor’; the process of looking ahead and reshaping code before jumping in to develop new features.

ex: Alex decided to prefactor the Selector component before adding the multi-select capability.

In this post I’ll take a look at how my team have settled on an approach to feature development that reduces the impact of improving code.

TL;DR - De-risk feature delivery by scouting possible challenges and restructuring existing code in advance.

Code has a tendency to grow organically

Many programmers would love to spend their work hours polishing their favourite module or library, but software development in the real world is about far more than producing perfect code. Shifting requirements, team constraints and looming deadlines can deny us the luxury of planning and overseeing every small change. And, left unchecked, these small changes can snowball into pockets of technical debt, where any new change is made more difficult by an uncooperative codebase.

Paying back technical debt is one motivator for refactoring, where the structure of the code is changed without adding new functionality. But refactoring can be time-consuming, and doesn’t offer any immediate business value, so it can be difficult to justify for its own sake. Furthermore, as a developer, it can be hard to stay motivated to refactor code that you may not touch again for years - and conversely, to decide when to draw the line and move on.

Even without technical debt, new features usually require structural change...

Today’s code won’t provide tomorrow’s feature

As a small, agile team, focused on delivering immediate value, we rarely write speculative code. In fact, we avoid it. Why?

  • Accurately predicting what features we might want to build in the future is incredibly difficult. It’s much easier to figure out what’s useful now, and build that - and it’s also guaranteed to deliver business value. Predicting the future is best left to astrologists and weather forecasters!
    See: YAGNI

  • Even if we’re confident in what we think we’ll need, choosing abstractions without concrete use cases is tough. Corner cases and emergent complexity go undiscovered until the right questions are asked; those questions only come to mind when we have a real use case to mull over.
    See: Rule Of Three

So, our codebase reflects the requirements of our product today (and may have a small hangover from yesterday too). But what about tomorrow?

Collaboration is hard

As developers, when we begin implementing a new feature, we’ll often find that the area was not designed with our new use case in mind - or we may even discover technical debt that further obstructs our progress. We may then be tempted to refactor the area as we go, changing it to suit our needs. For a solo programmer this poses no issues, but things are different in a team environment:

  • An unexpected delay can have a cascading impact. Suddenly one of your team members is busy decoupling the FluxCapacitor from the Discombobulator which means they can’t work on other sprint commitments. Anything depending on their work gets pushed back too, disrupting their teammates’ workstreams.

  • Refactoring often involves changing many areas, moving the ground under other programmers’ feet and interfering with seemingly unrelated work. Resolving these conflicts takes further time and effort.

  • When structural changes are made alongside behavioural changes, complexity multiplies in code review & debugging: a single reason for change becomes two reasons, which must be considered simultaneously.

These pressures can invite short-term thinking and rushed decisions that ultimately compound the problem.

Look before you leap

Agile practices encourage us to prioritise near-term wins over long-term punts. But we needn’t be totally myopic! Being smart about our upcoming work pipeline can save time, energy, and morale.

We noticed a recurring complaint in our retrospectives: large refactoring PRs that also added new behaviour were often the source of much frustration. We decided as a team that if we began working on a feature and found that the code needed to be restructured, that we would undertake ‘preparatory refactoring’, or prefactoring before adding the feature. This makes reviews and conflict resolution much easier, reducing the mental load of reviewer and merger.

Recently we’ve taken this process even further: before starting work on a new feature or epic, we nominate a scout to examine existing code, flush out unknown unknowns and verify assumptions. This has a low impact on the rest of the team, who are finishing off other pieces of work. The scout then has the opportunity to suggest some prefactoring before the rest of the team begin building, to ensure they’re working on solid foundations.

Ideally the scout should be a senior developer who has experience working in the given area. This isn’t about code ownership, but practicality: they will be more able to quickly identify potential issues. It’s helpful if they’re involved in product discussions early on, so they’re well-acquainted with the requirements, since sometimes technical constraints mean that a prospective feature has to be redesigned.

It’s a very simple (yet surprisingly helpful) approach, smoothing out development and keeping developers happy and productive.

Software development is still a young discipline, but new practices are evolving all the time. In my team our most valued principle is adaptability, being able to use whatever methodologies keep us productive as a team. Prefactoring helps us stay agile and deliver robust features, while dealing with the typical heritage of being a maturing start-up.

Discussion (0)

Editor guide