Refactoring web applications

In which we explain technical debt and a way forward

Does this sound familiar?

You've started this great new project and have been hammering out new features at a great pace. Lately your velocity has dropped. It was only a minor decrease in the beginning, but it's becoming a severe slowdown now. Now you're at a point where you feel that you're having to defend yourself to stakeholders on why it's taking so long to deliver new features.

You're probably dealing with something that's called tech debt. Tech debt is the accumulation of technological decisions that favor short-term velocity over long-term maintenance. It's the result of making choices that made you look good at the start of the project by cutting corners, but it's slowly becoming a more serious problem now that your project has matured.

Just like financial debt in the real world, it's better to pay off your debt before it accumulates in even more debt.

When working in software development, paying off your (tech) debt is called refactoring. During refactoring, you take existing code and change it in such a way that results in more maintainable, readable code. One important condition is that the external behavior of the code stays the same. In the case of this example, that means our features still perform the same pieces of functionality as before.

Note: Some people might tell you that "refactoring is only for people who write bad code in the first place". Please, ignore those people! It's simply not true. There's plenty of reasons why tech debt is introduced in a project, and I'd argue the most important one is agile development.

While working agile, you're dealing with constantly evolving requirements. You're building working software, releasing it to the world and based on feedback from going to production you'll re-iterate again. This, by definition, makes it impossible to design a scaleable, maintainable solution from the start.

An encompassing overview, which you'd need to make all the right choices from the start, of the product will only be possibly by the time that a substantial amount of time and energy has already been invested in the project already. Only by the time that the product contains a decent amount of consumer facing features will you be able to fully understand the results of your initial choices.

Approaching the refactor

Refactoring can sound like a daunting task. It can involve changing critical parts of your application in an all-or-nothing approach. That's why you have to treat it just like any other enormous issue in an agile project. Consider "refactoring" as an epic and break it up in tons of smaller stories. The goal is for every story to reduce tech debt, piece by piece.

Accept refactoring as a recurring part of your sprint cycle.

Steps to refactoring

  1. Create a list of annoyances/items that you want to solve. Involve the whole development team in these discussions. Do not let designers or product owners join these discussions. The idea is that developers can figure out on their own which parts of the codebase are blocking their progress the most. Let them own both the problem of tech debt, but more importantly the solution to these problems. Nothing is more empowering than knowing you can solve problems on your own.
  2. When doing sprint refinement, go over the refactoring list and discuss in broad strokes how you want to solve this issue.
  3. Deprecate methods or options. Use JSDoc to document which methods/classes you're deprecating and why. This helps with tools like IntelliSense. Also write down which alternative methods should be used instead, so developers will know what to do when confronted with the deprecation warning.
  4. Ensure you have a solid set of tests written for your deprecated methods, so you know when refactoring everything still works.
  5. Write a replacement method and apply it to at least one place of your codebase. When everything works as expected, refine the API. Take a step back. What annoyed you about the previous solution, and did you solve what you set out to do? If you're happy with the new API, write and/or port tests.
  6. Replace all other instances of the deprecated message too. Update tests/mocks where needed.
  7. Rinse and repeat.

Another way to get the message across is to use console.log to provide information to developers while they are in development mode. Take care not to ship this to production, as it can look unprofessional. For our React projects we've created a small utility hook called useDeprecationMessage that checks if you're running in development mode,

import { useEffect } from 'react'

function useDeprecationMessage(message, group = 'No group specified') {
  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      console.groupCollapsed(`Deprecation warning: ${group}`)
      console.trace(message)
      console.groupEnd()
    }
  }, [message])
}