Peer Code Review | When PRs Can't Be Small: Strategies for Making Large Code Changes Reviewable
- erinvh620
- Apr 7
- 9 min read
Intended Audience: software developers
TL;DR When a PR can’t be made smaller, it’s up to the PR author to ensure it’s still reviewable—because reviewable PRs lead to higher-quality reviews. This can be done by structuring changes into logical, well-scoped commits, keeping changes laser-focused, and distributing the review across multiple reviewers. These tactics help preserve reviewability—even for the biggest PRs.
In my previous post, I explained why it’s important to assess a pull request’s (PR) reviewability before jumping into the diff. A PR is reviewable when it's structured to minimise the reviewer’s cognitive load, allowing them to focus their limited energy on thoroughly scrutinising the proposed changes.
I try to keep PRs under 400 lines because smaller PRs are more reviewable.
But let’s face it: sometimes large PRs are unavoidable. When I’ve hit those moments—where splitting the changes would introduce more complexity than clarity—I’ve found that the key is ensuring the PR is still reviewable, even if it's large.
In this post, I'll walk through three common scenarios where large PRs typically occur:
Updating a widely-used dependency
A pervasive rename
Introducing a new formatting rule
For each of these cases, I'll share strategies to structure the PR effectively, reduce cognitive load, and keep the review process manageable—without sacrificing code quality or reviewer sanity.

Updating a Widely-Used Dependency
A new version of a dependency may introduce a variety of changes, such as new methods, removed methods, renamed classes, altered behaviours, changes in the default configuration, or even performance and security improvements.
Updating a dependency used pervasively throughout the codebase can result in a large PR. For instance, if a method signature has changed, you’ll need to update every instance where that method is called—which could easily touch hundreds or even thousands of lines. Breaking this kind of PR into smaller PRs is often infeasible since each merge into the main branch needs to leave the codebase in a compiling, stable, and fully functional state.
For simplicity, I'll focus on one common and relatively straightforward type of dependency update: changes to method signatures.
Consider a fictional example: You maintain a delightful Make-Your-Own-Sundae app, which allows users to design their own ice cream sundae. This app depends heavily on a third-party library called Flavours, which provides lists of available ice cream flavours. The Flavours library provides methods like:

Suppose a new version of the Flavours library is released. The new version adds a “dairy-free” parameter to the above methods. The new method signatures are as follows:

The task of updating the Make-Your-Own-Sundae app to use the latest version of Flavours has fallen to you. As you start making changes, you realise the PR is growing—fast. It’s well over 500 lines, and you're only partway through.
One way to handle a widespread update like this while keeping the PR reviewable is to update each method in a separate commit. For example:

Each commit should include only the changes necessary to accommodate the new method signature—nothing extra. No refactoring, no cleanup, no unrelated fixes. Structuring your commits this way ensures reviewers won’t have to differentiate between dependency-related changes and incidental edits, significantly reducing cognitive load during the review.
This approach also allows reviewers to evaluate the PR commit by commit, with each commit becoming trivial to review. Thus, the overall review process becomes far less overwhelming.
However, following this approach might still result in a PR containing thousands of changed lines. Regardless of how trivial each change might be, that’s still too much for a single reviewer to handle.
In such a case, consider distributing individual commits to different reviewers. For instance,
Ahmed could review the commit that contains the "getAllFlavours()" changes,
Bianca could review the commit that contains the "getNewFlavours()" changes,
Chen could review the commit that contains the "getBestSellingFlavours()" changes,
and Deepika could review the commit that contains the "getFeaturedFlavours()" changes.
By distributing the work, each reviewer maintains high scrutiny, ensuring thoroughness.
The Case for Wrapping Dependencies
In his book Clean Code, Robert C. Martin (aka Uncle Bob) suggests that dependencies should always be kept behind an abstraction layer. This way, instead of directly calling the dependency’s methods, your code only interacts with a thin layer you control.
Following this pattern allows you to entirely avoid a large PR when updating to a new version of the dependency. If you wrap the dependency, then when the dependency’s API changes, you need only update your abstraction layer, not the entire codebase. Your codebase has fewer “maintenance points”, as Uncle Bob calls them.
If we followed this advice, then when we first integrated Flavours into our codebase, we would do something like this:

This code wraps the "Flavour" type as well as the "getAllFlavours()" method, both of which are provided by the Flavours library.
In this case, no matter how many hundreds or thousands of times you call "getAllFlavoursFunctionWrapper()" in your code, migrating to the new method only requires a single line change, line 18:

If, later on, you want to start making use of the "dairyFreeOnly" parameter, you can introduce it by first adding a default value to your function signature:

You can do lots more from here. And none of it will require a large PR.
Other Types of Dependencies
Uncle Bob’s “wrap all dependencies” advice is practical when it comes to libraries, APIs, services, and sometimes frameworks. But it doesn’t extend to dependencies like programming language, compiler/toolchain, and runtime environment. You cannot feasibly “wrap” a programming language or a compiler.
However, in most mature ecosystems (Java, .NET, Swift, Python, etc.), deprecation comes before removal, often with clearly defined versioning policies. This gives developers time to adapt: first a warning, then eventual removal in a major version bump. This aligns with Semantic Versioning principles, which specify that breaking changes (like removing a method) must occur in a major release (e.g., 2.x → 3.0) and ideally with a warning.
For example, instead of immediately replacing:
getAllFlavours()
with:
getAllFlavours(dairyFreeOnly: Bool)
the maintainers of Flavours should release an intermediate version where both methods coexist:

By updating to this version of Flavours, each deprecated method can be updated in a separate PR, avoiding a large PR and thus significantly reducing the reviewer’s cognitive load. Problem solved!
Updating frequently helps avoid the chaos of jumping three major versions at once and having 20+ deprecations explode in your face. But it comes with tradeoffs. Frequent updates require engineering time, and newer versions—especially major releases—can be unstable. Ultimately, your team must decide what cadence makes the most sense for them.
A Pervasive Rename
The Make-Your-Own-Sundae app requires all users to create an account and log in before using the app. However, the team has decided to introduce a new concept: the guest user.
Guest users will be able to build their own ice cream sundae, but with limited access. They won’t be able to view the list of best-selling or featured flavours, and they won’t have access to new flavours at all.
To prepare for this change, the team has decided to rename all instances of "user" to "loggedInUser" throughout the codebase.
This may sound like a simple variable rename, but it’s not. The "user" identifier is pervasive, touching hundreds of lines and almost every file. And while some of the changes might be straightforward, others are more subtle. Consider this method:

We want to rename the "user" argument to "loggedInUser", but a refactoring tool won’t catch this. Similarly, consider a boolean like this:

This variable should be renamed to "isNewLoggedInUser" to reflect the new terminology. There might also be method names, constants, comments, and even file names that need updating for consistency.
That’s fine—but don’t get carried away.
This is not the place to rewrite comments for better grammar, reword method names for clarity, or do “quick” unrelated refactors. Save those for another PR. This PR has no room for excess. The goal is to make the rename as mechanical and predictable as possible so the changes are easy to review.
Unlike complex refactors or feature additions, a rename like this should be:
Easily verified: Reviewers can skim for consistency, not analyse logic.
Low cognitive load: No behaviour changes—just one-to-one substitutions.
How Do You Structure the Work?
Handling an extensive rename and keeping it reviewable can be tricky. Here’s what's worked for me in the past:
1. Do the messy work first.
Let the compiler guide you. Use refactoring tools. Rename things until the app builds and tests pass. Don’t worry about perfect commits—you’re in survival mode. Compiler-driven development is totally fine in this situation.
2. Once it compiles, clean it up.
Now’s the time to search for things the compiler won’t catch: method names, file names, comments, test descriptions—anything that still references the old terminology.
3. Then, structure your commits.
This is your chance to make the work clean and reviewable. Maybe you commit one directory at a time. Maybe you group changes by type—method names in one commit, variables in another. Use an interactive rebase or restage everything and commit intentionally. The goal is logical, digestible chunks.
4. Split the review across the team.
Even if the change is conceptually simple, it can be too large for one reviewer to handle well. Divide the review by folder, module, or commit range—whatever makes sense.
Why This Works
You’ll catch things you missed. Rebuilding clean commits forces you to look at every file again with fresh eyes.
You’ll make life easier for your reviewers. Smaller, well-scoped commits lead to faster, higher-quality reviews.
Trying to write perfect commits while wrangling compiler errors across 200+ files is a recipe for burnout. So don’t. Let it be messy at first, and then take the time to make it reviewable.
That’s what works for me, but whatever your process, structure it to support your reviewers. That’s how you get better reviews and a better result.

Introducing a New Formatting Rule
A seemingly simple change—adding a new formatting rule to your formatter—can result in a massive PR affecting hundreds or thousands of lines across the codebase.
Since this is purely a formatting change, it’s usually mechanical and easy to review, but it’s still important to structure the PR properly.
Suppose your team decides to enforce an import sorting rule in your formatter. The moment you apply the rule and auto-format existing files, your PR may touch every file in the repo—making it appear overwhelming at first glance.
Since this change affects many files but doesn’t change logic, the main concern is avoiding noise and structuring the PR in a way that makes review manageable:
1. Separate the Rule Change from the Reformatted Code
Make two commits:
Commit 1: Add the new rule (but don’t apply it yet).
Commit 2: Run the formatter and apply the changes across the codebase.
This separation makes it crystal clear what’s changing: reviewers can look at the rule itself first, then review the bulk changes separately.
2. Group Changes by File Type or Directory (If Needed)
If the auto-fix touches hundreds of files, consider multiple commits to keep things structured:
Commit 2A: Apply the rule to src/components/
Commit 2B: Apply the rule to src/utils/
Commit 2C: Apply the rule to test files
This allows multiple reviewers to divide the work, making the PR much more manageable to review.
3. Keep the PR Purely About Formatting
Since formatting changes should be mechanical, avoid mixing in unrelated changes (e.g., refactoring logic, fixing unrelated warnings). This ensures the PR remains trivial to approve—reviewers don’t have to scrutinise every file for hidden logic changes.
4. Flag the PR as “No Logic Changes”
To reassure reviewers, clearly state in the PR description that this PR only applies a new formatting rule—no functional code changes.
Why This Works
Reviewers don’t have to hunt for the actual rule change—it’s in its own commit.
The large, automated changes are easy to verify since they are purely mechanical.
If necessary, multiple reviewers can divide the review and approve parts independently.
Large linter or formatter update PRs don’t need to be painful—they just need structure. By separating the rule from its application and keeping the PR focused purely on formatting, you make it easy for reviewers to approve with confidence.

Give Fair Warning
The most important thing you can do to make any large PR more reviewable is to give the reviewer(s) a heads-up.
If you’re going to drop a large PR on your teammates—even a beautifully structured, low-cognitive-load one—give them a heads-up.
No matter how reviewable, it’s still a big chunk of work. Letting teammates know in advance allows them to block out time, manage their workload, and avoid being blindsided by a sudden wall of code.
A quick mention in standup is all it takes. Something like:
"Heads up—I’ll be opening a large PR in the next few days to rename user to loggedInUser. It should be easy to review since it’s commit-by-directory, but it’ll touch a lot of files."
That kind of courtesy builds trust and helps ensure your carefully prepared PR gets the attention it deserves. And if you’re planning to split the review across multiple teammates, giving early notice helps make that coordination smoother, too.

Final Thoughts
Large PRs are hard to review. Period.
But when they’re unavoidable, we owe it to our teammates—and the long-term health of the codebase—to structure them thoughtfully. A well-organized PR reduces review fatigue, improves code quality, and makes it more likely that reviewers will actually catch issues rather than skim.
It takes effort. It takes time. But it’s worth it.
Even when PRs can’t be small, they can still be respectful, reviewable, and high-quality.
If you’ve ever had to review (or write) a huge PR, I’d love to hear your strategies—or horror stories!
Another excellent blog post.. Enjoyable read. I like the idea of structuring it well in the first place, wrapping to keep code together logically and reduce energy required to make changes.