How to Write a Pull Request That People Actually Want to Review
by Eric Hanson, Backend Developer at Clean Systems Consulting
Why PRs Sit in the Queue
You've seen this: a PR that was opened three days ago, hasn't been reviewed, and the developer is frustrated. The delay isn't usually because reviewers are ignoring it. It's because the PR is hard to review.
Hard to review means: it's not clear what the change does, why it exists, what the testing story is, or where to start reading. The reviewer opens it, sees fifteen changed files with no context, and closes the tab to come back "when they have time." They never have time.
A PR that's easy to review gets reviewed quickly. Not as a matter of reviewer motivation, but because easy reviews get done during small windows of time — ten minutes between meetings — while hard reviews require uninterrupted focus that nobody has spare.
The Three Things Every PR Description Needs
What this PR does, stated plainly. Not the implementation details — the purpose. One or two sentences that a developer who hasn't been following the work can read and immediately understand.
## What
Adds idempotency key support to the payment charge endpoint to prevent
duplicate charges when the client retries after a network timeout.
Why this PR exists. The business reason, the incident that triggered it, the technical debt it addresses. This is the context the diff can't provide.
## Why
After the incident on March 14, we found that network timeouts were causing
clients to retry charge requests that had already succeeded. This resulted in
20 duplicate charges before monitoring caught it. Idempotency keys are the
standard solution (Stripe's API uses the same mechanism — see their docs on
idempotency).
How to verify it works. What tests exist. What you manually verified. What the reviewer should check if they want to validate the behavior themselves.
## Testing
- Unit tests: `PaymentGatewayClientTest#idempotencyKeyOnRetry`
- Integration: `PaymentFlowIntegrationTest#noDuplicateChargeOnRetry`
- Manual: tested with `scripts/simulate_timeout_retry.sh` against staging,
confirmed single charge appears in Stripe dashboard after simulated timeout
The Diff Is Not the Description
A common failure: the PR description is left blank, or just the commit message, with the implicit expectation that reviewers will read the diff and figure it out. This is technically possible. It's also the fastest way to get shallow reviews.
When a reviewer has no context, they review defensively — they look for obvious errors and style problems, because they can't evaluate design decisions without knowing what problem was being solved. You get comments on variable names and missing null checks, but not on whether the architectural approach is right.
When a reviewer has context, they review strategically — they evaluate whether the approach is correct for the problem, whether the edge cases are covered, whether the design will hold up as requirements evolve. That's the review that actually improves the code.
Self-Reviewing Before You Request Review
Before you hit "Request Review," read your own PR as if you were a reviewer who knows nothing about this work. Specifically:
Read the diff from the outside in — start with the highest-level changes (new files, changed signatures) and work toward the details. If you get lost, the reviewer will too.
Look for the code that requires context to understand. A complex conditional, a counter-intuitive data structure choice, a performance optimization that obscures intent. Leave inline comments on these explaining the reasoning:
# Using a sorted list here instead of a set because we need to maintain
# insertion order for the retry sequence — set iteration order is
# nondeterministic and would produce inconsistent retry behavior.
retry_sequence = sorted(attempts, key=lambda a: a.timestamp)
Inline comments in the diff are visible to reviewers as context, not as code changes. They're the most efficient way to pre-answer questions.
PR Size and Its Effect on Review Quality
A 1,500-line PR does not get the same quality of review as three 500-line PRs covering the same changes. The research on this is consistent: reviewer effectiveness declines significantly above 400 lines of diff (Cisco's internal study on peer code review found defect detection rates dropped sharply above this threshold).
This is not a reviewer failure. It's a cognitive load problem. Above a certain size, the reviewer is context-switching so frequently between files and concepts that they lose track of the thread.
If your PR is large because the feature is large, split it:
PR #1: Add database schema and migration for idempotency keys
PR #2: Add idempotency key generation to payment service
PR #3: Add idempotency key validation in gateway client
PR #4: Add integration tests and monitoring
Each PR in the sequence is reviewable in isolation. Reviewers can see the progression. Reviews can happen in parallel for the later PRs if the first one is already merged.
A Practical PR Template
GitHub supports PR templates via .github/pull_request_template.md. A minimal template that actually gets used:
## What
[One to two sentences on what this PR does]
## Why
[Why this change is needed — link to ticket, describe the problem]
## Testing
[What tests were added or modified, and what was manually verified]
## Notes for Reviewer
[Anything that needs attention, known tradeoffs, areas of uncertainty]
Keep the template short. A five-section template with required fields turns into a compliance exercise that gets filled with placeholder text. A four-field template that takes three minutes to complete gets filled with actual content.
The Metric Worth Tracking
Time from PR open to first review comment is a signal of how reviewable your PRs are. If it's consistently over twenty-four hours, either your team is overloaded or your PRs are hard to start reviewing. Both are fixable, but they require different interventions — and a good PR description is always part of the fix.