Tristan's Blog

“That some of us should venture to embark on a synthesis of facts and theories, albeit with second-hand and incomplete knowledge of some of them – and at the risk of making fools of ourselves” (Erwin Schrödinger)

Stacking

I believe strongly in merging small code diffs. In my experience, small code diffs merge more quickly, are released more quickly, leading to a faster feedback loop with end users - reducing the probability that work is invested into the wrong software.

There are other benefits: smaller code diffs are easier to review. Past a dozen files or a few hundred lines changed, the feasibility and quality of any code review plummets. Likewise for quality assurance: smaller diffs affect fewer systems, and are typically significantly easier to test, leading to lower risk for releases - decreasing the potential "blast radius" as my coworker calls it.

Stacking

This all sounds well and good, but is challenging in practice. Fixing a null pointer bug with a null check may be a line or two of code, but what about creating a brand new feature, or performing a large scale refactoring? How can I transform large code diffs into smaller ones?

One battle tested solution I use is code diff stacking, or PR stacking. What this basically means is a chain of git branches, a stack of code diffs that build on top of each other. In other words, break down the code diff across multiple diffs. For example:

develop <-- feature/friends-list-api <-- feature/friends-list-frontend

Where the first branch builds a web API, and the second builds the frontend consuming the API.

Why not one PR?

This is a good question! Sometimes, one PR is the right approach. I use these heuristics when approaching the question:

I would emphasize that stacking or splitting diffs is not a dogmatic approach, but a pragmatic approach, a tool to be used in the right circumstances.

Break it down

I typically approach stacking with the following approaches.

Vertical

Vertical stacking means each code diff is fully functional and can be released independently. This is an ideal approach when building an entirely new feature, which can be gated behind a feature flag.

For instance, I was tasked to build a new dispatchers page at work. This page was fairly dynamic, with a master/detail view of live driver routes, plus actions to handle on the road scenarios. I'll note there was a lot of uncertainty with this feature, so getting users on board by releasing early and often is key.

I broke the code diff down as follows. First, the master view, a pane displaying a list of driver routes. This was straightforward to build, as I only had to focus on building a solid foundation and displaying the list. Then, in a second diff, I built the detail view for the driver route. On top of that, more diffs followed, iterations on functionality and bug fixes from the previous diffs. This encouraged a strong feedback loop between users and devs, avoiding the unfortunate scenario where months of work go into a release that's ultimately rejected by users.

Horizontal

As opposed to vertical slices, horizontal slices are not independently releasable. Instead, the code diff is broken down into logical pieces, system by system; the small code diffs are merged together and then released all at once.

This can mean different things in different codebases and projects. A typical example is breaking down a feature into data model changes, followed by frontend changes, followed by backend changes, followed by internal changes. This is the approach I am currently taking for a project that's upgrading a previous feature, extending it to offer more options to the customer.

The first diff in my stack implements the database changes, with care paid to migrating existing data to new columns. The next builds out the updated customer user interface, with accompanying API changes. The next diff addresses necessary changes to internal operations systems. I approached the diffs this way because the underlying data model changes necessitated significant changes to several systems, that is the first "thin vertical slice" would frontload a disproportionate amount of code.

Multi stage

When reflecting on a few recent projects at work, and in discussion with other engineers, I thought of a new approach to stacking that combines horizontal and vertical. The motivation is when, as in the previous example, the initial "thin vertical slice" would be so large as to erase any potential gains from stacking.

In my example project, I realized that the customer user interface changes were backwards compatible. I could have worked on the interface changes first, aiming to release the changes, and then following the horizontal stacking for the remainder of the work. This would have meant the first code release would be significantly sooner, and the interface could be tested with customers, alongside further development.

A coworker has been working on a large scale refactor of our codebase to support multiple currencies. The first code diff was quite large, and contained a slew of different refactors, including to the data model, template display, billing, and so on. In hindsight, the large diff could have been broken down similar to my project above: for example, a first diff could refactor the template display of currencies to a universal interface - render_currency(). This could even be broken down further into batches of changes to templates, de-risking any individual diff. Finally, when the underlying data model is changed, that code diff can add support for rendering the new currencies by changing only one function - render_currency() - rather than hundreds of templates.

I think these two approaches are slightly different, and could be fleshed out further (and named?), but they share a common theme: identify changes that are in support of later code changes, either by frontloading backwards compatible changes, or refactoring to a universal interface.

Tips

Before wrapping up, let me note a few tips.

I use a rebase strategy for keeping my branches up to date. I have recently begun experimenting with single commit branches and interactive rebases to streamline this workflow further. I'll note that there are products that help with this, namely graphite.

When requesting reviewers, I may ask the same person to review the entire stack, just as if they were requested to review one large PR.

Conclusion

I find these practices to be useful and productivity boosting. I feel more confident in my code when diffs are small and easy to review and test. I look forward to continuing to hone this skill.