Confident coding: Making backwards incompatible changes less painful for all

Leanne Kerford
|
October 16, 2024
Moderne eases breaking changes
Contents

Key Takeaways

For companies with 100's or 1000's of repositories, it’s challenging and time-consuming for developers to understand the scope of repositories affected by their changes. This can result in small, well-meaning refactors costing many developers unexpected hours of work to fix their broken code. Ultimately, developers can become hesitant to make changes to libraries that are widely used, which can impact code quality, performance, and potential. 

Developers might take the manual path to search their source code management (SCM) system to see what repositories use the changed class or method, but this isn’t an exact science. Discovering the impact of changes quickly can become an impossible task if there are many classes or functions that are being modified. Simple tasks, like renaming or moving a class, deleting a deprecated function, or removing a no-longer-required input to a function, turn into tedious and time-intensive tasks, which results in them being avoided. Deferred actions like these continue to add to a company’s technical debt. 

The core of the problem comes down to not being able to easily see what downstream repositories would be affected by a change. What if developers could know what repositories would be affected by their change when they create a pull request? 

By knowing the full scope of their change, developers would be able to make informed decisions when making changes. Maybe they find that a simple refactor breaks 100’s of repositories. This allows them to make choices in their change—maybe they decide to move forward with their change as it is or maybe they change their approach. In any case, when the change is merged, they have all the information at hand when they make their change, and they can improve the experience for everyone impacted.

Determining the affected scope of a change

Using a service to manage the workflow of extracting this information allows us to enhance developers' workflows without adding to them. The only direct involvement required by the developers is to create the pull request and to review the data surfaced to them at the end of the workflow. 

To determine the affected scope of a change, there are three major parts:

  • Identifying what classes and functions have been modified
  • Determining downstream repositories using the modified entities
  • Summarizing and displaying results

This service workflow can be visualized in the diagram below.

Identifying what classes and functions have been modified

The approach taken to identify what has changed depends highly on the language and build tools that a repository uses. Let’s look at an example using Gradle to build a Java repository. The RevApi plugin can be used to extract the entities that have been modified. 

As part of the CI build, you can have the RevApi plugin publish a JSON file that can be read in to determine the entities that have been modified. By default the plugin will fail builds with breaking changes. Depending on the desired workflow, CI can accept all changes to avoid the failure or the developers must accept the changes using the RevApi flow. After we have extracted a list of entities that have been modified, we can move to the next step.

Determining downstream repositories using the modified entities

With our list of modified entities, we can leverage the Moderne API to create and run a search recipe to determine if and where any of the modified entities are. The Moderne API is a GraphQL API that enables services and users to query the Moderne Platform. This allows a service to orchestrate the creation and management of recipes without having to directly involve the developer. 

The list of modified entities provided by the previously generated JSON file, is split into methods and types that have been modified or deleted. This allows the service to create an appropriate search recipe for each modified entity in the form of a recipe YAML that describes what we want to search for, such as:

After generating the recipe YAML, the Moderne GraphQL API is utilized to start the recipe, using the mutation below: 

After the recipe has started, a service can start polling the associated recipe waiting for the recipe to complete. Recipe status can be polled using the following GraphQL query:

When a recipe is completed, we can move to the final step of summarizing results. 

Summarizing and displaying results of repos with incompatibilities

With the goal of enhancing developers’ existing workflows (not adding on to it), it’s useful to extract the findings of the executed recipes and post the results as comments on the PR. This meets developers where they are already working.

When a recipe is completed, the `totalResults` field shows if there are any results to return to the developer. If no results are found, a notification will be pushed to the SCM system, and a comment will appear on the associated PR informing the developer that they do not have to be concerned about downstream breakages. 

However, if at least one of the modified entities is used by a repository downstream, extracting more details from the recipe results is useful to provide more specific data to the developer, such as which repositories have a backward incompatible change and how many of them are present in the repositories. This information is aggregated and displayed to the developer in the form of a comment on their PR. The service fetches the information to present all these details to the developer using the query below:

Providing developers with the details of what downstream repositories will be affected is not meant to dissuade them from making changes, but rather to empower them with knowledge about the full scope of the change, allowing them to preemptively notify and provide migration instructions for affected teams.

Automating fixes for breaking changes

Understanding the impact of breaking changes is critical, but let’s take this one step further by leveraging the Moderne Platform. When making a change, the developer understands how many repositories downstream will be affected. If there are only a handful, they might make the updates manually. Now if there are 10, 20, or a couple hundred affected repositories, that’s where it gets more difficult. The developer could reach out to the owning teams and farm out the changes, but this doesn’t decrease the number of hours spent making the updates overall. It’s just spreading the pain. 

Another approach is that the developer making the change could go to the Moderne Platform and upgrade the associated dependencies by running a recipe to migrate all affected repositories. At this point, they reach out to the owning teams to get the pull request merged. This is a huge improvement from our starting point, but downstream teams might get multiple requests a day for changes like this. 

Could this be an even better experience? What if the developer included a recipe YAML with their change that could then be picked up and executed when the dependencies of the associated repositories are upgraded? This is all possible with Moderne.

Instead of developers pushing useful changes down the road, they can now more confidently improve systems. They can understand the full scope of repositories impacted by their code changes as well as actively support developers on the receiving end of breaking changes. Contact Moderne to learn how you can get started.