There are 3 main type of value that developers can add by changing a software system. They can write new features, which give users new capabilities. They can improve some cross-feature attributes of the system like loading speed, accessibility, design, logging or documentation. Or they can make architecture improvements that make it easier to make other changes. Generally engineers don’t have to argue to add features, because the business decision makers will be calling for them constantly. Cross-feature things like performance, design or logging sometimes come from business decision makers, but often only when things have become problematic, and then the conversations around how long it will take to achieve improvements can be tense ones. Architecture concerns pretty much never come from business folks, because they usually don’t see the effects of a bad architecture until it is too late and shipping simple features has become a disastrously slow process1. It’s part of a developer’s job to push for the latter 2 type of changes when they’re needed. But with limited resources, needed is often hard to quantify.
That’s why it is important to know what is actually causing you pain, or will soon be causing pain. Is your site getting slower and slower? Is your CSS a giant tangled mess that creates bugs with every change2? Make a list, and then prioritize it by importance and expected effort.
Once you have a list of things that you want to fix, it can be tempting to try to fit those fixes in all over the places. You’re creating a new page, might as well write it in Vue instead of Angular. You’re changing some styles, maybe you can use a CSS in JS library. But the absolute worst thing you can do with limited resources is try to improve too many things at once. You’ll end up with a codebase with many different ways to do everything, so that every change requires a full context reset, and maintenance means dealing with multiple sets of bugs and compatibility issues for each duplicated approach. That’s why prioritization is critical. With limited bandwidth, you need to know what one thing will be the best use of your time.
A few truths to note at this point:
The implication of these truths is that there is value in starting small and expanding. If we can pick a small section of the application that is cordoned off from the rest of the code in a meaningful way, we can create a foothold for the new technology. This serves as a learning opportunity, an example, and a starting place. If it goes badly, small changes are easy to revert or fix. If it works well, we can look for more similar interface boundaries, or expand out to convert some of the code that uses the new code.
When I converted a large Backbone app to use React, I took advantage of 2 different interface boundaries. I started by converting small “widget” components to be written in React, and came up with a method of calling these widgets from within Backbone code. Once I had enough of these building block widgets written in React, I started writing new pages in React. Then finally I would convert subsections of Backbone driven pages to React, starting from the bottom up and converting the top level layouts of the page last. I was using 2 interface boundaries: components and pages. That made sense for Backbone to React, where I was essentially converting one type of component tree to another. But the lines may be different for other changes, and you’ll need to use your judgement.
Not every change needs to be hard and slow. Sometimes we can take advantage of automation. Tools like Prettier and ESLint make it easy to automate code style and correctness fixes. Tools like React often provide codemods to help make it easier to update to a new version of the tool or use best practices. Unit tests can help you prevent regression bugs and are easy to automate with a CI server or pre-commit hooks.
If you’re dealing with problems that are more specific to your codebase, it may make sense to invest time into writing your own automation scripts, whether that means hacking together a crazy bash/python script to do a massive find and replace, or using a tool like Ansible to automate some previously manual developer processes. It made a big difference in our company when our QA developer put in the work to automate the creation of consistent local environments.
With a small team, some improvements may simply not be worth it over time. Genuinely valuable things like test coverage, JSDoc style automated documentation, old browser support, add overhead to future changes as well as any up front cost to implement. These are the type of things that advocates tend to moralize about, and say that it’s the developers job to advocate for them. And that is true as I alluded to earlier. But when faced with limited resources, it’s important to prioritize, and understanding the cost of maintaining certain improvements is part of that.
Along the same lines, as developers we’re often encouraged to think of ourselves as craftsmen. Think of Steve Jobs talking about using good wood for the back of a cabinet or the aforementioned moralizing about code and process quality. There are great things about this, since it is a developers job to look after the quality and craftsmanship of their product. But it also is good to align that craftsmanship with business needs and create something that is useful rather than merely theoretically beautiful. So there is a place for making changes to improve the “purity” of code, but when investing limited resources, it’s even more important than usual to balance that with business needs.