One PR, One Purpose #
There are PRs that make reviewers open a new tab, grab a drink, then come back and ponder: where should I even start? The PR title says “update auth service”, but the diff contains a model refactor, a bug fix in the payment flow, three dependency updates, and a new endpoint. Which should be reviewed first? Is the payment bug fix related to the auth refactor, or just coincidentally in the same PR? These questions should never exist — and the One PR, One Purpose principle exists to prevent them.
This principle is simple in formulation but requires discipline in practice: one Pull Request should have only one clear logical purpose, stateable in one sentence, and reviewable as one cohesive unit.
What Does “One Purpose” Mean? #
“One purpose” doesn’t mean one file or one function. One purpose means one intent that can be independently verified and explained without mentioning other changes in the same PR.
// ✓ A PR with one clear purpose
"Fix token refresh logic to prevent premature logout"
→ Every change in this PR serves one purpose: fixing token refresh
→ If reverted, only token refresh returns to its previous state
→ Reviewers know exactly what to test and verify
// ✗ A PR with mixed purposes
"Update auth service"
→ What was updated? Everything? Part of it?
→ Reviewers don't know where to focus
→ If a bug is introduced, it's hard to determine which change caused it
The simplest test for evaluating PR focus: can this PR’s purpose be explained in one specific sentence? If you need “and”, “plus”, or “also” to explain its purpose — the PR likely already has more than one purpose.
// ✗ Sentences indicating a PR has too many purposes
"This PR fixes the login bug AND refactors the auth service AND updates the JWT dependency"
"This PR adds an export feature AND fixes the dashboard layout"
// ✓ Sentences indicating a focused PR
"This PR fixes the race condition in concurrent order updates"
"This PR adds rate limiting to the public search endpoint"
"This PR refactors the payment service to the strategy pattern"
Why Does One Purpose Matter? #
A PR Is a Communication Medium, Not Just a Merge Mechanism #
A good PR isn’t just about correct code — it’s about changes communicated clearly. Reviewers, other engineers reading git history, and even your future self all depend on a PR’s clarity to understand why a change was made.
flowchart LR
subgraph PRWithManyPurposes
A1[Reviewer opens the PR] --> B1[Reads a 15-file diff]
B1 --> C1{"Is this a refactor or feature?\nWhat should be focused on?"}
C1 --> D1[Uncertainty → shallow review]
D1 --> E1[LGTM without confidence]
end
subgraph PRWithOnePurpose
A2[Reviewer opens the PR] --> B2[Reads the description: fix token refresh]
B2 --> C2[Knows exactly what to verify]
C2 --> D2[Focused, meaningful review]
D2 --> E2[Approve with confidence]
endReviewer Cognition Works on Context, Not Diff #
When reviewing code, reviewers don’t read line by line like a compiler. They build a mental model of what changed and why. Clear context is the fuel for that mental model.
A PR with many purposes forces reviewers to build several mental models at once — and human working memory capacity is limited. When context is too much and fragmented, what happens is:
Cognitive load effects on reviewers:
A PR with 1 purpose → a single cohesive mental model
→ reviewers can detect edge cases and wrong assumptions
→ high-quality feedback
A PR with 3 purposes → 3 mental models to build and maintain
→ energy spent just separating contexts
→ feedback becomes shallow: "the code looks fine"
A PR with 5+ purposes → cognitive overload
→ reviewers give up building a complete mental model
→ LGTM without meaningful review
Precise Rollback Is Only Possible with Focused PRs #
When there’s a problem in production, fast, precise rollback is the most valuable asset. A focused PR enables surgical rollback — only the problematic change is reverted, without dragging along other changes that were actually fine.
Scenario: A bug is found in production after deployment
A PR mixing many purposes:
PR contains: fix A, refactor B, feature C
Bug originates from: refactor B
Rollback options:
→ Revert the entire PR → the already-correct fix A and feature C are lost too
→ Don't roll back → the bug stays in production
→ No good options
A focused PR:
PR 1: fix A (already merged, no problems)
PR 2: refactor B (this is the problem)
PR 3: feature C (already merged, no problems)
Rollback options:
→ Revert PR 2 → only refactor B reverts, fix A and feature C stay safe
→ Precise rollback within minutes
Meaningful Git History Is Free Documentation #
The git log of a team applying One PR, One Purpose reads like a clear change journal. Every commit and PR tells one cohesive episode in the system’s evolution.
// ✓ Meaningful git history
abc1234 feat(payment): add retry mechanism for network timeouts
def5678 fix(order): resolve race condition in concurrent status update
ghi9012 refactor(auth): extract token validation to dedicated service
jkl3456 chore: upgrade grpc-go to v1.62
// ✗ Uninformative git history
abc1234 update code
def5678 fix bugs and add stuff
ghi9012 misc changes
jkl3456 PR from feature branch
This difference looks simple, but its impact is real: an engineer joining 6 months later can understand the system’s evolution from the first git log, but must read every diff in the second git log to get the same information.
Types of Purpose Contamination #
Understanding how PR purposes get contaminated helps engineers detect and avoid it earlier.
Contamination type 1: Opportunistic #
Happens when an engineer opens a file already touched for the main PR’s purpose, and “might as well” fixes other things they see:
// Opportunistic contamination scenario
An engineer is fixing a bug in auth/handler.go.
While opening the file, they see:
- A confusingly named variable
- An unused import
- An outdated comment
Tempted to "clean it up while I'm here" — adding changes
unrelated to the bug fix in the same PR.
// Why this is a problem
Reviewers see a diff mixing bug fixes and cleanup
→ Hard to verify whether the cleanup affects behavior
→ If problems arise after merging, hard to determine the root cause
// Solution
Create a separate ticket for the cleanup
Or make a separate PR after the bug fix PR merges
Contamination type 2: Dependency Chains #
Happens when a new feature requires a refactor first, and both get combined into one PR:
// Dependency chain contamination scenario
Task: Add multi-currency support to the payment service
Needs: Refactor PaymentService from singleton to instance-based first
✗ What often happens:
PR: "Add multi-currency support"
→ Refactor PaymentService (500 lines)
→ Add CurrencyConverter (200 lines)
→ Update the payment API (150 lines)
→ Update tests (100 lines)
Total: ~950 lines with two very different contexts
✓ What should happen:
PR 1: "refactor(payment): convert PaymentService to instance-based"
→ Purely structural refactor, behavior unchanged
→ Existing tests still pass as proof the refactor is safe
PR 2: "feat(payment): add multi-currency support"
→ On top of an already-clean foundation
→ Reviewers focus only on currency logic, not singleton implementation details
Contamination type 3: Slipped-in Housekeeping #
Happens when non-functional changes like dependency updates, code reformatting, or mass variable renames get combined with functional changes:
// ✗ Slipped-in housekeeping
PR: "Fix payment timeout bug"
Changed files:
- internal/payment/handler.go (the actual fix — 15 lines)
- go.mod (3 dependency updates — unrelated)
- internal/payment/models.go (variable rename — unrelated)
- README.md (documentation update — unrelated)
Reviewers must separate what's relevant to the bug fix
from what's just housekeeping
// ✓ What should happen
PR 1: "fix(payment): increase timeout for large transaction processing" (15 lines)
PR 2: "chore: upgrade payment-related dependencies" (if truly needed)
Contamination type 4: Scope Creep During Development #
Happens when a PR’s scope grows during development because of finding things that “need fixing”:
// The familiar scope creep cycle
Day 1: PR for fixing A
Day 2: "While at it, I'll also fix B which is related"
Day 3: "B turns out to need refactor C first"
Day 4: "While refactoring C, I'll add test D"
Day 5: PR ready for review: A + B + refactor C + test D = 600 lines
// Signals that a PR is experiencing scope creep
→ The PR title no longer describes the entire PR content
→ Adding files that are "not directly related" but "also need changing"
→ The PR has been running more than 3 days and keeps growing
// How to stop scope creep
Commit the existing changes
Create tickets for newly discovered changes
Finish the in-flight PR
Then start a new PR for the additional changes
How to Apply One PR, One Purpose #
Define the Purpose Before Opening the Editor #
The most effective step for preventing contaminated PRs is defining the purpose before writing a single line of code.
Questions to answer before starting to code:
1. What's the one thing I will change?
→ "Fix token refresh so it doesn't expire prematurely"
2. How do I know this PR is done?
→ "Users are no longer suddenly logged out after 30 minutes of activity"
3. Which files will most likely need changing?
→ "auth/token_service.go and auth/refresh_handler.go"
4. Is there a refactor needed before this can be implemented?
→ "Yes — TokenService needs extracting first. That's a separate PR."
Answering these four questions defines the PR scope
before coding starts — and scope creep becomes far easier to avoid.
Use the PR Title as a Commitment #
Write the PR title before starting to code and treat it as a commitment about what will be in the PR. Any change the title can’t describe shouldn’t be in this PR.
// Title as a commitment
Title written at the start: "fix(auth): prevent premature token expiry on active sessions"
During coding:
→ Find another bug in the auth service: create a ticket, don't fix it in this PR
→ Want to refactor a helper function: make a separate PR after this finishes
→ Want to update a dependency: that's not this PR's purpose, make a separate chore PR
Result: the submitted PR exactly matches the title written at the start
Split by Change Type #
A simple guide for separating changes:
flowchart TD
A{Change type} --> B["Structural only\nRefactor, rename, reorganization"]
A --> C["Behavioral\nFeature, bug fix, performance"]
A --> D["Non-functional\nDependency, format, docs, test"]
B --> E["PR: refactor/\nExisting tests must still pass"]
C --> F["PR: feat/ or fix/\nNew tests added"]
D --> G["PR: chore/ or docs/\nUsually can run in parallel"]
E -.->|foundation for| F| Type | Prefix | Can Be Mixed With |
|---|---|---|
| Structural refactor | refactor: | Can’t be mixed — always its own PR |
| New feature | feat: | Not with refactors or unrelated bug fixes |
| Bug fix | fix: | Not with refactors or new features |
| Dependency update | chore: | With other dependency updates, not with code |
| Format / cleanup | style: | With other cleanups, not with logic changes |
| Documentation | docs: | With other docs, not with logic changes |
Use a “Parking Lot” for Ideas That Arise While Coding #
While coding, there will always be things you see that feel worth fixing. Instead of immediately adding them to the in-flight PR, create a “parking lot” — a temporary place for those ideas:
// A simple, effective parking lot
Option 1: TODO comments with tickets
// TODO(PROJ-456): this variable name is confusing, rename in a separate PR
Option 2: Create tickets directly
Open Jira/Linear → create a ticket → note the ticket number → keep coding
Option 3: Draft PRs
Commit changes to a new branch → open as a Draft PR → return to the main PR
What must NOT be done:
→ Working on it in the same branch
→ "I'll remove it before the PR" — that never happens
Rescuing an Already-Too-Large PR #
It isn’t always possible to avoid an already-too-large PR. When this happens, there are several ways to rescue it:
flowchart TD
A[An already-too-large PR] --> B{"Is there a part\nthat can be split\nwithout breaking the rest?"}
B -- Yes --> C[Split into several small PRs]
C --> D[Merge the most independent first]
D --> E[The remaining PR becomes smaller and focused]
B -- No --> F{"Is this big PR\nlegitimate?"}
F -- Yes --> G["Add a review guide\nto the PR description"]
F -- No --> H["Reconsider the scope\ncan it be split with feature flags?"]Steps to rescue an already-too-large PR:
1. Identify the most independent changes
→ Usually: dependency updates, formatting, variable renames
2. Move them to a new branch
git checkout -b chore/cleanup-from-feature-x
git cherry-pick <relevant-commit>
3. Submit as a separate PR — this can usually merge right away
4. The original PR becomes smaller and more focused
5. If still too large: consider feature flags to
allow the independent parts to merge into main earlier
Anti-Patterns to Avoid #
// ✗ Ambiguous titles permitting unlimited scope
"Update auth service" → anything can go into this PR
// ✓ Specific titles defining clear boundaries
"Fix token refresh to prevent premature logout"
// ✗ "Might as well" while working on a PR
"I'm already in this file, let me fix the other thing too"
// ✓ Create a ticket for what you find; don't work on it in the same PR
// ✗ Refactors and features in one PR
Reviewers can't separate what changes structure
from what changes behavior
// ✓ Two PRs: refactor first (old tests must pass), then the feature on top
// ✗ Dependency updates mixed with logic changes
Update grpc-go + add a new endpoint in one PR
→ Hard to determine whether a bug comes from the new dependency or the new logic
// ✓ Separate: a chore PR for the dependency, a feat PR for the new endpoint
// ✗ Waiting for the "perfect" PR before splitting
"I'll split it once everything is done"
→ Never happens because too much needs moving
// ✓ Split from the start, before changes pile up
One PR, One Purpose Checklist #
BEFORE STARTING TO CODE:
□ PR purpose defined in one specific sentence
□ PR title written and will serve as the scope commitment
□ Identified: does a refactor need to happen before implementation?
□ If yes: the refactor becomes a separate PR that must finish first
WHILE CODING:
□ Every new change that arises evaluated: does it fit the PR's purpose?
□ Changes that don't fit go to the parking lot (tickets / draft PRs)
□ No "might as well" changes unrelated to the main purpose
□ The PR title is still accurate after all changes are added
BEFORE OPENING THE PR FOR REVIEW:
□ Read the entire diff and confirm every changed file is relevant to the purpose
□ No files changed because "they happened to be open" — not because of the PR purpose
□ The PR title can still describe the entire PR content in one sentence
□ The PR description focuses on one purpose — no "also", "plus", or "and"
SIGNS OF A FOCUSED PR:
□ Reviewers can understand the PR purpose before reading a single diff line
□ If this PR were reverted, only one logical change would be lost
□ All added tests relate to the PR's main purpose
Summary #
- One PR, One Purpose isn’t about size — it’s about focus — a 500-line PR can still have one clear purpose if all its changes serve the same goal.
- The simplest test: can the PR purpose be explained in one sentence without the word “and”? — if it needs more than one sentence, the PR likely already has more than one purpose.
- Define the purpose before opening the editor — this is the single most effective step for preventing scope creep and purpose contamination.
- The PR title is a commitment — any change the title can’t describe shouldn’t be in this PR.
- Separate refactors from features — this is the most fundamental separation and the most often violated. Refactors change structure; features change behavior — both need different review types.
- Housekeeping (dependencies, formatting, renames) is always a separate PR — mixing it with logic changes makes bug root causes hard to trace.
- Use a parking lot for ideas arising while coding — tickets, draft PRs, or TODO comments with ticket numbers — not directly working on the same branch.
- An already-too-large PR can be rescued — identify the most independent parts, move them to separate PRs, and the main PR becomes more focused.
- One PR, One Purpose is about respecting reviewers — a focused PR says: “I’ve thought seriously about this change’s boundaries so you can review effectively.”