As someone who's been working with software for a while now but only recently started experimenting with stacked pull requests, it's been quite the learning experience. Throughout my journey, I've observed some common strategies for using them effectively on my team.
So, I thought I'd share some of the insights I've gathered along the way - whether you're a seasoned developer looking to up your game or a newcomer looking to make your mark in the industry, stick around. Because when it comes to mastering the art of stacked PRs, I've got you covered.
Before you start stacking
Before you start doing any stacking, it can be useful to think about your repository’s architecture. How is your code structured? What are the different functional components of your codebase?
As an example, a common layout for simple web apps might be to split the code into a layered architecture. It could consist of a database layer, a backend service, and a frontend service.
Once you note the different functional components your codebase is comprised of, think about how those components interact - there will typically be some sort of dependency graph. Let’s go back to the example of simple layered architecture: before you can add a new feature to the frontend, support for that feature will likely be needed in the backend and new features to the backend will frequently need updates to the database layer first.
Having a good understanding of the architecture of your codebase will help influence how you organize your stacks and provide good insights into how they should be split.
With that in mind, let’s talk about five common models we see for splitting stacks!
Functional component-based stacks
The most straightforward way to stack changes is to build a feature to completion while either breaking up and stacking the major components along the way, or splitting the commits up once you've finished building the feature.
A straightforward way of stacking your changes is to build a feature and then split it into a stack with PRs for all the major components. For example, if I have a full stack feature I’m building, I might create: 1 PR with any database changes, 1 PR with changes to the backend, 1 PR with front-end changes using the backend changes, and a final PR with integration tests that exercise the change as a whole.
By splitting the change up like this, reviewers can focus their attention on just one part of the change at a time. Additionally, if you have reviewers have expertise in a certain subsystem, they can just focus on that part of the review.
Iterative improvement stacks
A popular model we frequently see on our team is to have a rolling stack of iterating improvement. In this model, as soon as I have a change large enough to be interesting, I would create a PR and put it up for review. While waiting for that change to be reviewed, I can continue to make improvements that will eventually create the next PR in the stack. This can be continued until the full feature being built has been completed.
A common pattern in this model is to address PR feedback with changes later in the stack. Especially for feedback that is not critical, but nice to have, a new PR can be created on top of the stack addressing the feedback. Since it is just focused on the previous feedback, it can be much easier for the reviewer to confirm that the feedback was addressed appropriately.
One mode to watch out for with this type of stack is waiting too long before merging any of the PRs. Remember until a PR is merged, there is a risk that conflicting changes could be made that require the PR to be reworked. Also, you don’t need the full stack to be complete to start merging it. When a set of PRs are ready to be merged, feel free to merge what is ready while you continue to work on the stack.
Refactor/change
This is one of my favorite models. I find this particularly useful when fixing bugs. When fixing bugs, I frequently find myself refactoring the code around where the bug lives. It is frequently useful in helping to find the bug, can help make the code easier to read, and can sometimes make the bug more obvious. When making changes like this, I like to perform the refactoring in 1 PR and then the actual bug fix in a separate PR stacked on top.
By making this separation, it makes it very explicit to reviewers what part of the changes are refactoring and what part of the changes are the actual bug fix.
Version bumps/generated code
A common software development task is updating library versions or generating code. These types of updates are usually not particularly interesting or risky. They tend to just create noise in code review that can distract reviewers and make it more difficult to determine the more meaningful parts of the change.
By separating out these types of changes into their own PR and then stacking the changes that use the updates in another PR on the stack, reviewers will have a much easier time understanding what part of the code is just boilerplate and what part of the code has the more meaningful changes.
Riskiness
An unfortunate reality of software development is that we sometimes need to revert changes. There are a number of reasons for needing to revert: bugs, different system loads or behaviors in production, unexpected edge cases, or a number of other issues.
Ideally, if you need to revert a change, you want to revert the smallest amount of code possible. That way the revert does not impact unrelated code or features. Stacks can provide a powerful way to do this.
If you worry that some of the changes you are making are riskier than others, you can pull the risky parts of the change into their own PRs, but still keep them part of the stack they belong to. Then if they do cause issues, you are able to revert the risky PR(s) while still keeping the code that landed in the other PRs in production.
To wrap things up, here are some key takeaways to keep in mind when working with stacked PRs:
Only stack related changes together. If there's no dependency between PRs, consider creating separate stacks to avoid delays or blocking unrelated changes.
Merge changes frequently to detect integration issues early. Stacked PRs can help by allowing you to push out small changes early for review and testing.
Restack frequently on trunk to detect conflicts early. Pull in changes from the main branch and rebase your stacked PRs to avoid conflicts down the line.
By following these tips, you can make the most of stacked PRs and streamline your development cycle. Happy coding!