VelocitySignals
Stacked PRs — The Git Workflow I Wish I Knew Earlier
By Darshil Shah · Nov 2026 · 6 min read
- Git
- Pull Requests
- Code Review
- Developer Workflow
- Stacked PRs
I was proud of that PR.
It was a full User Management feature: API layer, state management, UI, tests, all of it. Twenty-three files. Around 1,500 lines. I had spent two weeks on it, tested every flow and opened the PR feeling like I had just handed in finished work.
Then nothing happened.
Day one, no comments. Day three, still nothing. On day five I pinged my reviewer and he said he would take a look that week. On day nine, the first comment finally arrived:
“Can we split this? I’ve opened it three times and I don’t know where to start.”
I will be honest: my first reaction was irritation. The code worked. Splitting it felt like busywork. I remember thinking, just read it top to bottom, it’s not that complicated.
It took me a while to admit he was right. The problem was not the number of lines. I was asking him to hold the API design, the state layer and the UI in his head at the same time and to judge whether all three were right in a single sitting. Nobody reviews well like that. They either skim and approve or they procrastinate. He procrastinated. Honestly, I would have too.
That PR is the reason I started using stacked PRs.
## So what is a stacked PR?
Instead of putting a whole feature in one branch, you split it into small branches where each one is built on top of the previous one, not on main.
What I had been doing:
```text
main → feature/user-management → 1,500 lines → one PR
```
What I do now:
```text
main
↓
user-api
↓
user-state
↓
user-ui
↓
user-tests
```
Each PR does one thing and each one is reviewed only against the branch it actually builds on.
## How it works in Git
Start from main like usual:
```bash
git checkout main
git checkout -b feature/user-api
git add .
git commit -m "feat: add user API"
```
Here is the only real change: for the next piece, do not go back to main. Branch off the branch you are on.
```bash
git checkout -b feature/user-state
git commit -m "feat: add user state"
git checkout -b feature/user-ui
git commit -m "feat: add user UI"
```
A branch is just a movable pointer to a commit. The stack is really one straight line of commits with a few labels sitting at different points on it.
When you open the PRs, you set each one’s base to the branch below it:
- PR #1: `user-api` → `main`
- PR #2: `user-state` → `user-api`
- PR #3: `user-ui` → `user-state`
That is the whole stack.
## Why reviews suddenly got faster
GitHub builds a PR’s diff against the merge base, the nearest common ancestor of the two branches. Because `user-state` is based on `user-api`, PR #2 only shows the state changes. My reviewer never has to scroll past API code he already approved.
Here is the same feature both ways:
```text
Before: PR #1 → everything +1,480 / −210 (23 files)
After: PR #1 → API +180
PR #2 → State +240
PR #3 → UI +520
PR #4 → Tests +390
```
Same total code. Completely different experience for the reviewer. The first PR in my next stacked feature got reviewed the same afternoon, because a 180-line API change is something you can finish before your coffee gets cold.
## Feedback shows up when it is still cheap
On that nine-day PR, one of the comments eventually said the API response shape should be different. That one comment rippled through the state layer, the UI and half the tests. Fixing it took almost as long as writing it the first time, because everything downstream was built on the old shape.
With a stack, the API is reviewed first. If the shape is wrong, I find out before 500 lines of UI depend on it. The loop becomes API → review → fix → state → review → UI and feedback arrives right when the decision is made, which is exactly when it is cheapest to change.
That alone would have saved me most of a week.
## The part nobody warns you about: rebasing
I would love to tell you stacking has been smooth since day one. It has not.
The first time a reviewer asked me to change something in `user-api`, I amended the commit, pushed and felt great. Then I opened PR #2 and it was a mess: duplicated changes, conflicts in files I had not touched.
Rebasing or amending rewrites history. Git creates a brand-new commit with a new SHA, but the branches above it are still pointing at the old commit. They are now built on something that no longer exists in the branch below them.
The old fix was to rebase each branch by hand with `--onto`. Do that for four branches and you will start questioning your life choices.
Since Git 2.38, there is a much better way. From the top of your stack:
```bash
git rebase main --update-refs
```
It moves every branch pointer in the stack along with the rebase. I liked it enough to make it the default:
```bash
git config --global rebase.updateRefs true
```
Two more lessons I learned the hard way:
- Use `--force-with-lease`, never plain `--force`. Stacking means force-pushing a lot. Once, a teammate pushed a small fix to one of my branches and I overwrote it with a `--force` without knowing it was there. `--force-with-lease` refuses to push if the remote has changed since you last fetched.
- Turn on `git rerere`. When you rebase a stack, you hit the same conflicts over and over. `rerere` remembers how you resolved them and replays the fix automatically:
```bash
git config --global rerere.enabled true
```
## And the one that got me twice: squash merging
If your repo squash-merges PRs, stacks break. Squashing PR #1 creates a brand-new commit on main that none of your stacked branches know about, so PR #2 suddenly shows the API changes again plus a pile of phantom conflicts.
The fix is either to merge with merge commits or rebase-merge or to rebase the next branch onto main right after the squash lands. Tools like Graphite, git-town and Sapling/ghstack exist mostly to automate exactly this restacking and base-branch retargeting. I did it manually for a few months before trying one and I would recommend trying one sooner than I did.
## When I do not stack
Stacking is not free and I have over-used it.
At one point I stacked a small bug fix into three PRs because I had gotten excited about the workflow. My reviewer asked why he was reviewing three PRs for a 40-line change. Fair question.
For “fix button alignment,” a normal PR is fine. Stacking earns its cost when a feature has real layered dependencies: API → state → business logic → UI → tests. My rough rule now: if the change touches more than one architectural layer, it is probably a stack. If it does not, it is probably not.
And one honest caveat: stacking does not fix a badly split feature. If your “API” PR secretly depends on decisions you have not made yet in the UI, you have just made a big PR with extra steps.
## What actually changed
The biggest change was not a Git command. It was how I think about pull requests.
Before, a PR was where I submitted my finished work. A kind of “ta-da” moment.
Now, a PR is a message to another developer. Its job is to be understood. And the smaller and more focused that message is, the faster I get a useful answer back.
Looking back at that nine-day PR, the reviewer was not slow. I had written him a 1,500-line message with no paragraphs and asked him to reply all at once.
## What we learned
Writing code is only part of the work. Making it easy for someone else to understand, review and trust matters just as much.
Stacked PRs add Git complexity. You will mess up a rebase at some point. But for a big feature with dependent layers, I will take small PR → review → small PR → review over one 1,500-line PR every single time.
Next time you are about to open a massive PR, try splitting just the first layer out and see how long it takes to get reviewed.