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.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Why Restricting Developer Access Kills Productivity

Locking down environments seems safe, but it often backfires. When developers can’t access what they need, projects stall—and frustration grows.

Read more

The Difference Between Latency and Throughput and Why Both Matter

Latency and throughput are different properties of a system, optimized by different techniques, and often in tension with each other. Confusing them leads to performance work that improves one metric while silently degrading the other.

Read more

Sinatra vs Rails — When I Reach for the Smaller Tool

Rails is not always the right Ruby web framework — Sinatra earns its place for a specific class of services where convention overhead is waste and explicit routing is an asset, not a liability.

Read more

Your System Is Only as Fast as Its Slowest Part

Performance work that ignores the actual bottleneck is the most common form of wasted optimization effort. Finding and eliminating the constraint is the only optimization that matters.

Read more