Learn how small pull requests improve review, feedback, rollback safety, and architecture—and how to split real features without breaking the system.
Small Pull Requests Are a Technical Strategy, Not a Team PreferenceA “complete feature” pull request feels efficient.
One branch. One ticket. One review. Database migration, API, business logic, UI, tests, and cleanup all arrive together. The author gets to present a finished story instead of explaining several intermediate steps.
The problem is that the first serious feedback also arrives together.
By then, a questionable API shape may already be used by the UI. A risky migration may be buried under generated files. A reviewer who disagrees with the architectural direction must now ask for changes across the entire stack. The feature may be complete from the author’s perspective, but it is expensive for the team to understand, change, merge, release, or reverse.
Small pull requests are not a matter of reviewer taste. They are a way to control technical risk.
DORA’s guidance on working in small batches connects smaller units of work with faster feedback, easier remediation, and safer AI-assisted delivery. Google’s engineering practices make the same idea concrete at code-review level: a good change is focused, includes its related tests, gives the reviewer enough context, and leaves the system working after it is merged.
That last condition matters most. “Small” is not a line-count target. It is a design constraint.
A large pull request often optimizes for the person writing the code while pushing cost onto everyone after them.
The author keeps all the context in one branch and avoids thinking about intermediate compatibility. The reviewer receives the opposite experience: many decisions, several kinds of risk, and no obvious place to start.
That creates predictable failure modes:
None of this means that a 40-line change is automatically safe or that a 600-line change is automatically bad. A tiny authorization mistake can be dangerous. A large generated migration can be mechanically simple.
The useful question is not “How many lines changed?” It is “How many independent ideas must a reviewer hold in their head at once?”
I use one practical definition:
A self-contained pull request makes one coherent change, proves what that change does, and leaves the repository safe to merge and deploy.
That usually means the pull request has:
Self-contained does not always mean immediately visible to an end user. An optional database field, a new service seam, or a disabled code path can be a complete enabling change when it is independently testable, backward-compatible, and useful to the next slice.
It is also possible to go too small. Adding an unused interface in one pull request and its only implementation in another can make both reviews harder. Google’s guidance explicitly warns against changes so fragmented that their implications are hidden. Small changes still need enough context to be understood.
“Database PR, backend PR, frontend PR” sounds like a plan. Sometimes it is. Sometimes it is only a list of folders.
A better split starts with behavior and risk:
Vertical slices are useful when a thin user behavior can cross the stack safely. Horizontal or layer-based slices are useful when they establish a stable contract that lets later work proceed. The label matters less than the post-merge state.
The mistake is splitting mechanically and hoping the pieces become coherent on their own.
Imagine an application that sends every weekly digest at a fixed UTC time. We want users to choose an IANA time zone such as Africa/Cairo, and we want the scheduler to send the digest at 09:00 in that local zone.
This touches the database, API, scheduler, settings UI, tests, rollout configuration, and eventually old compatibility code. It could easily become one “complete” pull request.
Here is a safer sequence.
The first pull request is deliberately boring. It separates structural movement from behavioral change. If the scheduler refactor changes an output, the reviewer knows the cause is in that small diff—not mixed with a new API and UI.
The second pull request expands the contract without requiring every caller to change at once. A PostgreSQL migration might begin with:
ALTER TABLE user_preferences
ADD COLUMN digest_time_zone text;
The application treats NULL as the old UTC behavior. New code can write the field; old code can continue without it. This is safe only after checking the real client and serialization contracts—some consumers reject unknown fields, and some deployment systems run old and new application versions at the same time.
The final constraint belongs later, after the data is backfilled and all active application versions can supply the value:
ALTER TABLE user_preferences
ALTER COLUMN digest_time_zone SET NOT NULL;
That command is valid PostgreSQL, but “valid SQL” is not the same as “safe production migration.” Table size, existing data, locks, traffic, database version, and rollback requirements still matter. PostgreSQL’s current ALTER TABLE documentation explains options such as adding some constraints as NOT VALID and validating them separately to reduce the impact on concurrent work.
The important pattern is expand, migrate, then contract. Do not rename a required column, deploy code that only understands the new name, and call the combination atomic unless your deployment environment truly makes it atomic.
Different kinds of change fail in different ways. They should not all use the same splitting recipe.
If you rename, move, and redesign behavior in one diff, reviewers must reconstruct which lines are mechanical and which lines change the result.
Start with characterization tests when the existing behavior is not already protected. Then move or extract code without changing its public behavior. Only after that should a later pull request introduce the new rule.
For larger replacements, Branch by Abstraction can create a temporary seam between the old and new implementations. The seam has a cost, so remove it when the migration finishes.
Production rarely switches every process, worker, and client at the same instant. A safe schema sequence assumes old and new code may overlap:
A code rollback and a data rollback are not the same thing. Reverting application code does not automatically undo a backfill, restore deleted data, or make an old binary compatible with a contracted schema.
Additive changes are easier to stage than replacements. Introduce a new optional field or endpoint, deploy consumers, observe usage, and remove the old contract only after you know active clients no longer depend on it.
This is not permission to keep every version forever. Compatibility has a carrying cost. Give deprecated paths an owner and a removal condition when you add them.
A release flag lets you deploy completed internal slices without exposing an unfinished journey. Test the flag-off path, the flag-on path, and the transition between them. Keep the check near the feature entry point instead of scattering conditionals across the codebase.
Flags are temporary architecture. If there is no cleanup pull request, removal condition, or owner, the “temporary” branch becomes another permanent state the team must understand and test. Martin Fowler’s feature-toggle guidance is useful precisely because it discusses both release flexibility and the complexity created by multiple toggle states.
Small changes do not require an author to wait idle after every review request.
With stacked pull requests, each branch builds on the branch below it:
main
└── pr-1-scheduler-boundary
└── pr-2-time-zone-contract
└── pr-3-flagged-scheduler
└── pr-4-settings-ui
Reviewers see the difference between adjacent layers instead of repeatedly reviewing the whole feature. The stack documents dependency order, and the author can continue working while lower changes are reviewed.
The discipline does not disappear. Each layer still needs a focused purpose, passing checks, and a safe merge state. If PR 2 cannot function without PR 3 landing immediately, the stack has hidden a broken intermediate state rather than solved it.
Stacking also adds overhead: lower-layer changes can ripple upward, reviewers need the dependency order, and rebasing several branches can become tedious. At the time of writing, GitHub documents native stacked pull requests, including CI and bottom-up merging, but the feature is in public preview and its tooling constraints may change. The workflow principle is older than any one platform feature.
A reviewer should not have to ask what was tested, which risk matters, or where this change sits in a sequence.
A compact description can be enough:
## Purpose Persist and validate an optional digest time zone. ## Behavior - Existing users continue to receive digests on the UTC schedule. - New clients can save an IANA time-zone identifier. - Unsupported identifiers return a validation error. ## Not in this PR - Scheduler behavior - Settings UI - Backfill and final constraint ## Risk and rollback Additive nullable column. Application rollback keeps the column unused. No data is deleted or transformed. ## Evidence - Migration test - API contract tests for old and new payloads - Repository round-trip test - Focused command: ./gradlew test --tests '*DigestPreferences*' ## Dependency Builds on PR #101. Next: flagged scheduler behavior.
The exact template is not important. The information is.
This extends the idea from my Claude Code and Android CLI workflow: generated code is a proposal; the toolchain and observed behavior are the proof. A small pull request gives that proof a clear boundary.
Useful evidence depends on the change:
Do not paste a wall of green logs into the description. Summarize the result and link to durable evidence. My article on developer portfolios and engineering decisions makes the same distinction: a claim becomes useful when the reader can follow it to an artifact and a reproducible check.
There is a deeper benefit to this practice.
If a feature cannot be introduced without changing the database, API, scheduler, and UI in one synchronized merge, the system may be telling you that its boundaries are too rigid.
The effort to create a small, safe slice encourages useful design properties:
This does not mean adding an interface around every class. Abstraction has a cost. It means noticing when delivery is difficult because one decision is entangled with five others.
My architecture patterns overview discusses boundaries between responsibilities. Pull-request design is where those boundaries meet daily reality. If the boundary exists only in a diagram and cannot support an incremental change, it is probably not doing much work.
Coding agents can produce a complete cross-stack implementation before a human reviewer has seen the first design decision. That increases output. It does not increase review capacity or reduce the cost of a wrong assumption.
Give the agent one behavioral slice, its compatibility rules, explicit out-of-scope items, and the checks that must prove completion. Review that result before asking for the next dependent slice.
Also inspect scope drift. An agent may upgrade dependencies, rename nearby types, reformat files, or “clean up” unrelated code while implementing a small feature. Those changes are not free just because they were generated quickly.
DORA’s current small-batch guidance calls this practice a safety mechanism for AI adoption. I agree with the direction, with one qualification: small batches only help when humans can understand and verify them. Ten opaque pull requests created faster than anyone can review them are still a queue of risk.
Some diffs are large for reasons that do not map cleanly to review effort:
Do not pretend those changes are small by distributing the same decision across arbitrary pull requests.
Instead:
Large can be justified. Unstructured cannot.
Before coding:
Before opening each pull request:
Before merging:
Small pull requests do more than make reviewers happier. They shorten the distance between a decision and feedback, give failures a smaller search area, make rollback boundaries clearer, and force the architecture to support change in safe stages.
The goal is not to maximize the number of pull requests. It is to minimize the number of unrelated decisions inside each one.
The next time a “complete feature” becomes a huge diff, do not start by asking which files can move to another branch. Ask which behavior can become safe, useful, and provable first.
What is the hardest kind of feature for your team to split without leaving the system in a broken state?