You know the one. It arrives in your review queue on a Tuesday afternoon: a pull request titled "fixes", diff spanning 2,347 lines across 41 files, no description, and a cheerful "PTAL" in the comment. Your heart sinks. You open it anyway. Twenty minutes later you've read the same method three times, still can't tell what problem it solves, and you've left a single nervous comment: "looks good overall, maybe rename this variable?" You've just rubber-stamped code you didn't understand.
The author wasn't malicious — they were probably heads-down, proud of the work, and just forgot that shipping code is only half the job. The other half is getting it reviewed, understood, and merged safely. A pull request isn't a formality you clear before merging; it's a conversation, and like all conversations, it goes better when someone puts in the effort to be understood.
This article is about that effort: how to write PRs that your teammates genuinely enjoy reviewing — small, purposeful, easy to follow, and easy to say yes to.
A PR is communication, not just code
Before a single line of diff, a pull request is a message to another human. That human has limited time, limited context about what you were thinking at 11 p.m., and a dozen other things competing for their attention. Your job as the author is to make their job easy.
Think of the reviewer as your audience, not your judge. They're not there to catch you out — they're a collaborator helping you ship something solid. The more you help them understand the why behind the change, the faster and better the review will be. A great PR description is like a cover letter for your code: it sets the scene, explains the motivation, and tells the reviewer exactly what to look at.
This mindset shift changes everything. Once you stop writing PRs for yourself and start writing them for your reviewer, you'll naturally want to make them smaller, describe them better, and clean them up before asking for eyes.
If a reviewer has to read the code to understand why the change exists, the PR description hasn't done its job. The code explains what changed; the description explains why it needed to change.
Keep it small and focused
The single most effective thing you can do to improve your pull requests is make them smaller. The larger a PR, the less thoroughly it gets reviewed, the longer it takes to merge, and the more bugs slip through.
A good PR contains one logical change. Not one ticket, not one feature, not one sprint's work — one coherent idea that can be understood, tested, and reverted independently. "Add user avatar upload" is one change. "Add user avatar upload and refactor the profile controller and bump three dependencies" is three changes wearing one coat.
How to split big work
Large features don't have to arrive as large PRs. Some practical splitting strategies:
- Separate refactoring from behaviour changes. First PR: rename, restructure, extract — no new logic. Second PR: the actual feature. Reviewers can verify the refactor has no side effects, then focus entirely on the new logic.
- Ship the infrastructure first. Add the database column, the migration, the type — all with no UI. Then add the UI that uses it. Each PR is independently deployable.
- Use feature flags. Merge incomplete work behind a flag that's off by default. The PR is small and safe; the feature isn't "done" until you flip the flag.
- Stacked PRs. PR B depends on PR A. Some teams use stacked-PR tooling to review each layer independently. If your team doesn't use stacking, keep it simple: finish and merge A first, then open B.
If you can't describe your PR in a single sentence without the word "and", it probably contains more than one logical change. Find the seam and split it.
Writing a title and description worth reading
The title is the headline. It should say what changed and why it matters, in one line. Avoid vague verbs like "update", "fix stuff", or "changes" and aim for specificity. Here are common PR title smells and how to fix them:
| PR smell | What's wrong | Better version |
|---|---|---|
| "fixes" | No context whatsoever — reviewer must read every line to guess what's happening. | "Fix race condition in cart checkout when concurrent sessions collide" |
| "WIP: various changes" | Signals the author isn't ready. Draft PRs exist for a reason. | Open as a GitHub Draft until it's genuinely ready for review. |
| "Update UserService" | Describes where, not what or why. Every PR updates something. | "Add email-change verification flow to UserService" |
| "JIRA-1234" | Forces the reviewer to open a second tab just to understand the motivation. | "Limit API rate to 100 req/min per IP (JIRA-1234)" — ticket as context, not as a substitute. |
| "Refactor + new feature + bump deps" | Three PRs in a trenchcoat. Each concern deserves its own review. | Split into three separate, focused PRs. |
The description is where the real communication happens. A good description answers three questions every reviewer has in the first ten seconds:
- What changed and where?
- Why was this change needed — the context, the bug, the user story?
- How do I verify it works — steps to test, screenshots, affected edge cases?
Here's a before-and-after to make it concrete:
--- BEFORE (the 2,000-line "fixes" PR) ---
Title: fixes
Description: (empty)
--- AFTER (the same change, written for a reviewer) ---
Title: Fix race condition in cart checkout on concurrent sessions
## What
Two concurrent checkout requests from the same user could both pass the
inventory check before either decremented the stock count, resulting in
overselling. This PR adds a row-level DB lock around the stock decrement.
## Why
Customer support flagged 12 oversell incidents in the last 30 days (see
Sentry issue #4421). The root cause is two requests racing between the
SELECT and the UPDATE on the inventory table.
## How to test
1. Run the new concurrency regression test: npm test -- cart.concurrency
2. In staging: open two browser tabs, click "Buy" simultaneously on a
product with stock = 1. Only one should succeed.
## Risk / notes
- Lock is scoped to a single product row; does not affect throughput
when different products check out at the same time.
- No migration needed — uses SELECT FOR UPDATE on the existing table.
Commit hygiene
How you organise your commits matters — not just to satisfy process, but because commits are the permanent record of how a change came to be. A year from now, someone (probably you) will run git blame on a line and follow the commit hash into the history. What they find should tell a story, not read like a stream of consciousness.
A good commit message follows the conventional format: a short imperative subject line (50 chars or fewer), an optional blank line, and an optional body explaining why — not what the diff already shows.
# The conventional format:
type(scope): short imperative subject
Optional body: why this change was made, what problem it solves,
any non-obvious decisions and the reasoning behind them.
# Real examples:
fix(cart): add row-level lock to prevent oversell on concurrent checkout
feat(auth): add email-change verification flow
refactor(profile): extract avatar upload to dedicated service
Beyond message format, think about commit shape. Each commit should represent one coherent step — a refactor, a test, a new behaviour. A reviewer who looks at individual commits (rather than the whole diff) should be able to follow the logical progression: "extract method → add test → implement behaviour" tells a better story than twenty "WIP" commits squashed at the last second.
Use interactive rebase to clean up a messy branch before opening the PR. You don't need perfect commits from the start — polish them before you ask for eyes.
Self-review before requesting review
Before you ping a teammate, become your own first reviewer. Open the diff on GitHub (or your code host of choice) and read it as if you'd never seen the code. This context switch catches more issues than you'd expect: leftover debug output, a function that grew too large, an import you forgot to remove, a comment that used to be true.
A self-review checklist worth running through:
- Read the diff top to bottom. Don't skim. Pretend you're seeing it for the first time.
- Check for debug leftovers.
console.log, TODO comments marked "remove", commented-out blocks, temporary hardcoded values. - Verify tests are there. If you added logic, is there a test? If you fixed a bug, is there a regression test?
- Look for unintended changes. Whitespace churn, reformatted files, accidental scope creep. Move them to a separate PR or revert them.
- Leave guiding comments yourself. If a section is non-obvious, add a PR comment explaining the reasoning before your reviewer sees it. This is not a sign of weakness — it's consideration.
If you wouldn't feel comfortable with a senior engineer reviewing the PR cold right now — without your explanation — it's not ready to go out. Fix it first, then request review.
Self-review also pays a selfish dividend: it catches embarrassing mistakes before they become permanent comments in the review history. Everyone has a story about the typo they spotted ten seconds after hitting "Request review". Ten minutes of self-review eliminates most of those.
Responding to review
A PR isn't done when you push the code — it's done when it merges. Between those two events is the review cycle, and how you handle feedback matters as much as the code itself.
Some principles that keep reviews moving and relationships intact:
- Reply to every comment. Even if only to say "done" or "good catch, fixed in latest commit". Silence leaves reviewers wondering whether you saw their note.
- Describe your fix when you push it. "Fixed in latest commit — moved the logic into a helper and added a test" is far clearer than a new commit with no context.
- Resolve threads thoughtfully. In most teams, the commenter resolves when they're satisfied. Don't self-resolve without acknowledging feedback. If you're uncertain about the convention, ask.
- Disagree respectfully. If you think a suggestion is wrong, say so clearly and with reasoning — don't just ignore it. "I considered that approach, but [reason] — happy to discuss if you feel strongly" keeps the conversation professional and productive. See also: how to review code kindly for the reviewer's perspective on the same conversation.
- Re-request review explicitly. After addressing feedback, don't wait for reviewers to notice. Re-request review and leave a short summary: "Addressed all comments. Main changes: extracted the util, added two tests, kept the original approach for X because Y."
How PR norms scale by team size
What makes a great PR isn't fixed — it shifts as the team grows and the codebase matures. Here's how the expectations evolve:
| Team context | What matters most | Practical norms |
|---|---|---|
| Solo / freelance | Future-you is the reviewer. PRs are documentation of decisions for when you return to the code in six months. | Even with no reviewer: a clear title, a brief "why" in the description. Commit messages that make git log readable later. |
| Small team (2–8) | Speed matters. Everyone knows the codebase. Reviews are conversational. Trust is high. | Keep PRs small and fast-moving. Descriptions can be lighter — a sentence or two of context. Async review with same-day turnaround is a reasonable expectation. |
| Mid-size team (10–50) | Reviewers may not know the full context. Onboarding is constant. Changes span teams. | Full what/why/how descriptions. Screenshots for UI work. A test plan. Cross-team changes need extra care and possibly wider review. |
| Large / enterprise (50+) | Reviewers are sometimes strangers. Compliance, audit trails, and correctness reviews matter. Turnaround SLAs exist. | Formal PR templates enforced by tooling. Required approvals. Security review for sensitive changes. Links to design docs and tickets. Rollback plan for risky changes. |
A startup team of four doesn't need a rollback plan in every PR description. A bank does. Match the ceremony to the stakes, and revisit the norms as the team grows — what worked at ten engineers will feel inadequate at fifty.
Key takeaways
- A PR is a message to a human. Write it for your reviewer, not for yourself. Their time and context are limited.
- Keep it small and focused. One logical change per PR. Separate refactoring from behaviour changes. Use feature flags to merge incomplete work safely behind a toggle.
- Title + description = orientation. Answer what changed, why it needed to change, and how to verify it. The code shows the what; the description provides the why.
- Clean commit history tells a story. Conventional messages, logical commits, a tidy rebase before you request review.
- Self-review first. Read your own diff on GitHub before anyone else does. Leave guiding comments. Catch the obvious mistakes yourself.
- Close the loop on feedback. Reply to every comment, re-request review after addressing notes, and disagree with reasoning — never with silence.
- Match ceremony to team size. A four-person startup and a 500-person bank need different PR norms. Revisit yours as you grow.
The next step is the other side of the table: how the person receiving your PR should give feedback that helps rather than stings. If you've ever written a carefully crafted PR only to get back a terse "this is wrong, rewrite it", you'll appreciate Feedback That Lands — the follow-up to this piece.