The problem is not the code
I build Flutter apps. A normal feature touches four layers at once.
- Models and json parsing
- Repository and API client
- State management
- Screens and widgets
When I ship that as one branch, the pull request is somewhere between 1,000 and 4,000 lines. And then one of two things happens.
Either the reviewer opens 40 files, scrolls for ten minutes, and writes "looks good". That is not a review. That is a signature.
Or the reviewer does the job properly, takes three days, and leaves a comment on the models file. Now every screen above it has to change.
The writing was fast. The review was slow. That is the real bottleneck.
The old workaround
You already know it. Split the work into branches yourself.
git checkout -b feat/booking-models
git checkout -b feat/booking-repo # branched off models
git checkout -b feat/booking-state # branched off repo
git checkout -b feat/booking-ui # branched off state
This works for about one day. Then a reviewer asks for a change in feat/booking-models, and you rebase three branches by hand. Then it happens again. Most people give up and go back to the giant branch.
What stacked pull requests change
On July 30 2026 GitHub put stacked pull requests into public preview.
The idea is small. A stack is an ordered series of pull requests. Each pull request targets the one below it instead of targeting main. Only the bottom one targets main.
PR #4 screens and widgets -> targets PR #3
PR #3 cubits and state -> targets PR #2
PR #2 repository and api -> targets PR #1
PR #1 models and parsing -> targets main
Because each pull request only contains its own layer, opening PR #3 shows you the state management diff and nothing else. Not the models. Not the widgets.
The Flutter example
Say I am building a booking flow. Here is the same feature, sliced.
PR 1 - models and json parsing. Around 190 lines. Targets main.
class Booking {
final String id;
final DateTime startsAt;
final BookingStatus status;
const Booking({
required this.id,
required this.startsAt,
required this.status,
});
factory Booking.fromJson(Map<String, dynamic> json) => Booking(
id: json['id'] as String,
startsAt: DateTime.parse(json['starts_at'] as String),
status: BookingStatus.values.byName(json['status'] as String),
);
}
Pure data. A reviewer can check the json keys and the null handling in five minutes.
PR 2 - repository and API client. Around 310 lines. Targets PR 1.
class BookingRepository {
BookingRepository(this._client);
final ApiClient _client;
Future<List<Booking>> fetchUpcoming() async {
final res = await _client.get('/bookings?filter=upcoming');
return (res.data as List)
.map((e) => Booking.fromJson(e as Map<String, dynamic>))
.toList();
}
}
Now the backend minded reviewer has something to read. Endpoints, error handling, caching. They do not need to wait for my widgets to exist.
PR 3 - cubits and state. Around 240 lines. Targets PR 2.
class BookingCubit extends Cubit<BookingState> {
BookingCubit(this._repo) : super(const BookingState.initial());
final BookingRepository _repo;
Future<void> load() async {
emit(const BookingState.loading());
try {
emit(BookingState.loaded(await _repo.fetchUpcoming()));
} catch (e) {
emit(BookingState.error(e.toString()));
}
}
}
This is where most real bugs live. Loading states, race conditions, what happens when the user leaves the screen mid request. It deserves its own focused review, and here it gets one.
PR 4 - screens and widgets. Around 380 lines. Targets PR 3.
The UI layer. The designer or the mobile reviewer looks at spacing, accessibility, and empty states. Nothing else is in the diff.
Same feature. Same total lines. Four reviewers can now work in parallel instead of one reviewer working through everything in sequence.
How you build a stack
Install the extension.
gh extension install github/gh-stack
You can also work with stacks on github.com, in the GitHub mobile app, or with a coding agent through the gh-stack skill.
The manual shape is the same idea you already know. Create a branch and open a pull request for your first layer. Then branch off that, and open the next pull request with the previous branch as its base.
gh pr create --base main # PR 1
gh pr create --base feat/booking-models # PR 2
gh pr create --base feat/booking-repo # PR 3
gh pr create --base feat/booking-state # PR 4
The difference is that GitHub now understands this is one stack. Every pull request shows a stack map at the top so a reviewer can see which layer they are looking at and where it sits.
Merging
This is the part that used to hurt, and it is the part that got fixed.
- Merge the latest ready pull request and it lands, along with every unmerged layer below it, in one operation.
- Or merge only one or more lower layers. The pull requests above stay open and automatically rebase and retarget.
No manual rebase chain. And your existing branch protections and required checks still govern what reaches main, so none of your rules change.
Merge queue support is rolling out progressively over the coming weeks.
When this is worth it
Not every change needs a stack. A bug fix is a bug fix.
It pays off when:
- The feature crosses architecture layers, which for mobile is most features
- More than one person should review, and they care about different things
- The bottom layer is stable and could ship on its own value
It is probably overkill when the whole change is under a couple hundred lines, or when you are the only reviewer anyway.
The actual takeaway
We spend a lot of energy trying to write code faster. AI made that part much faster for a lot of teams, which is exactly why review became the visible bottleneck.
Stacked pull requests do not make you write faster. They make your work reviewable. On a real team that is usually where the days are lost.
If you build mobile apps, I would like to know: what is the biggest pull request anyone has ever asked you to review, and did you actually review it?
Top comments (0)