🏄 Surfing the JavaScript Wave: Embracing Incremental Change in Real World Software Projects

JS Logo On Top of Wave

The JS ecosystem moves forward at a break-neck pace. New paradigms, frameworks, and tools are released seemingly every day. React Hooks are a recent example of this, with many software houses dropping tools in a race to rewrite their codebase to use the new techniques. But what do you do if you’re in a small team, managing a non-trivial sized codebase, with limited time to invest in staying on the bleeding edge?

Deciding to adopt new technologies is not a straightforward process. It’s a constant battle of weighing up the pros and cons of emergent tools and technologies; considering both the accumulation of technical debt, the potential risks of early adoption vs. the potential for huge productivity or product value gains.

In real-world scenarios, it is often not appropriate to drop tools and rewrite all your existing code to utilize something new. A balance must be found between keeping your knives sharp and still delivering a constant flow of business value. How can we balance these two seemingly incompatible workflows?

Grab your surfboard and ride the wave…

A Process for Incremental Change

In the Mobile & Practice squad at DataCamp, we have embraced a methodology of incremental change. Using a combination of simple tools and techniques we believe we have a flexible approach to keeping the knife sharp whilst still delivering business value on time: we focus on unlocking potential incrementally.

Whilst we do not have a formal procedure, a pattern has emerged over time around these steps.

  1. Learn & Understand
  2. Decide & Communicate
  3. Embrace Incrementally

Let’s take a deeper look at what this means.

Learn & Understand

We start with a discussion, focused around the goal of reaching a decision on how to move forward with a new technical initiative. For instance, when React Hooks dropped we were excited about the possibilities, but we resisted the temptation to drink the Kool-Aid and stop product development to re-write our codebase without first undertaking a real-world look at the actual benefits (Render Props - remember those?).

We start with an exploratory phase of learning about new technology. This is often in the form of pair/mob programming sessions. We start with a blank canvas and aim to get something basic working end-to-end. This will then develop into a further session where we take an existing portion of our production codebase and evaluate what happens if we adapt it to use the new technology.

We ask ourselves important questions:

  • What are the advantages of doing it this way?
  • Are there any (hidden) costs?
  • Is this a “viral” change - does it implicitly “spread” to other components

Just to give you an idea of what kind of things are in scope for these sessions, here are some recent sessions we have paired or mobbed on:

  • React Hooks (and therefore Functional Components)
  • End-to-end testing with Detox
  • Reducing “style debt” by using font primitives
  • Streamlining our workflow with Docker Compose

With our new learnings fresh in our minds, we can make a decision on if and how to proceed.

Decide and Communicate

Once we are satisfied with the benefits/costs of adopting new technology into our codebase, we can align on how we will embrace it incrementally.

We recently formalized our adoption of Architectural Decision Records (ADRs) as a tool for aligning-on and communicating our decision to adopt certain technologies. We even have an ADR for our use of ADRs!

The ADR is a simple markdown file and is initially introduced to the codebase as a proposal in the form of a PR. Using a PR allows us to discuss the proposal and further develop our shared understanding/knowledge around the technology.

The ADR will make it clear how we will adopt the technology moving forward and in the meantime documents the value and costs we learned about in the “Learn & Understand” phase. This way, future contributors (often our future selves) can refer to the ADRs to understand the context behind a particular decision.

Embrace Incrementally

Here’s the fun part! Now we get to use the technology in the codebase. So, now we’re going to drop everything and re-write everything from scratch? Wrong!

By using some simple off-the-shelf tools and strategies we can ensure that we embrace the change incrementally and sustainably.

Custom Lint Rules

Assuming the cost is low enough, we are big fans of introducing new eslint rules that we can configure as warnings. This allows us to assess our progress towards our goal of fully embracing a new approach over time. If a lint rule is not available for the particular use-case we have, we will also consider writing one ourselves.

For instance, if we are deprecating an old way of doing things we can use something like the react/forbid-elements rule to mark old-style components as deprecated with a warning.

Another nice side-effect of using the lint rule approach is that it lends itself to tidying things up as you go along: The Boy Scout Rule.

Boy Scout Rule

One of my favorite software engineering principles is the Boy Scout Rule often cited by Uncle Bob (Robert C. Martin). The notion is very simple:

“Always leave the campground cleaner than you found it.”

When applied to our methodology of embracing incremental change, this can be paraphrased as something like this:

When touching older code related to the code you’re currently working on, clean it up and update it to use your modern engineering best practices.

A busy campsite

This is our main way of embracing change incrementally: ensuring that new code is written to our new standard, whilst refactoring and updating old code as and when we touch it.

A Real-World Example: Functional Components

A real-world example in the DataCamp mobile app is our adoption of Functional Components as a preferred approach to implementing components (with some exceptions of course).

After a thorough analysis and reaching a shared understanding of the costs and benefits of introducing Functional Components - and subsequently React Hooks - into our codebase, we set about introducing change incrementally. We began by aligning around an ADR that described our best practices for developing React components: focusing on why we prefer Functional Components, the advantages (and disadvantages) of using them over Class Components and when exceptions could be made. We also outlined our approach for adopting them in our codebase.

There were many instances of the class components already being used and we couldn’t justify dropping tools to fix all of them. Rather, we focused on writing new components using only the new approach and adopted a strategy for incrementally reducing the debt over time using an eslint rule:

"deprecate/member-expression": [
  "warn",
  {
    "name": "React.Component",
    "use": "We prefer to use Functional Components: refer to ADR #0002."
  }
]

A warning is displayed when a developer inherits from React.Component and warnings will also be produced for all existing infringements of this new rule. This allows us to prevent the problem from getting worse whilst also allowing us to monitor the reduction of the problem over time:

Using custom lint rules

Note that we also refer to the corresponding ADR within the warning itself: this helps to clarify the reasoning behind the rule for developers who may not have encountered this before, and also our future selves.

Using a combination of linting and the Boy Scout Rule as above, we are able to ensure that new components are written to the latest standard whilst simultaneously reducing the debt/embracing the change across the rest of the codebase. Eventually, when the number of lint warnings related to this rule are small enough, we can potentially just briefly focus our efforts on reducing the count to zero and making the warning into an error to prevent regressing on the debt.

Whilst this example is fairly simple, I hope it helps demonstrate how the process can be used to introduce incremental change across your projects with a clear adoption strategy whilst also promoting clarity for the reasoning behind the changes. By introducing change incrementally we are able to keep up with the latest developments of the JS ecosystem, whilst avoiding the duds and still continuously delivering business value.

Conclusions

In this article, we’ve looked at a simple process that you can adopt to embrace incremental change in your codebase. It’s not intended as a one-size-fits-all or a dogmatic mantra but rather as a loose framework which we use to embrace incremental change within our codebase:

  1. Learn & Understand
  2. Decide & Communicate
  3. Embrace Incrementally

Keeping on top of the latest developments from the JS ecosystem is no easy feat. From a commercial software perspective, it’s a constant tradeoff between keeping your knife sharp yet still continuously delivering business value. However, with the right process and a surfboard, you can stay afloat!

Is this a wave you’ve also surfed? Let us know what techniques and approaches you’ve come up with to tackle this problem in the comments…