#How to Make Your Pull Requests Easy to Review
Code review is about to become your team’s biggest bottleneck. Everyone is shipping AI-generated pull requests now, which means three times the diffs hitting the same number of reviewers. The queue grows, nothing merges, and your velocity quietly grinds to a halt. In this tutorial we’ll fix that from the author’s side: instead of hoping a human catches everything, you bake a set of adversarial review rules into your own change before anyone else looks at it. We’ll cover three rules - keep the diff small, attack your own code, and ship observability - with Go examples for each.
If you prefer video, I’ve covered this exact material on YouTube:
Prerequisites
Before we get started, you’ll need:
- A working knowledge of Go - the examples are in Go, but the rules apply to any language
- Git and a pull-request workflow (GitHub, GitLab, etc.)
- Optional: an AI review tool you can run locally against your branch, such as Claude Code’s
/code-reviewcommand
Why AI Is Turning Code Review Into a Bottleneck
For years the constraint on shipping was writing the code. AI assistants have largely removed that constraint - but they’ve moved the bottleneck downstream. A single engineer can now open five pull requests in the time it used to take to open one, and every one of those still needs a human to read it, understand it, and trust it before it merges.
Reviewers don’t scale the way generation does. So the highest-leverage thing you can do is not “review faster” - it’s make each change fast to review. A pull request that’s small, self-defending, and observable gets a real review and merges in minutes. A sprawling, opaque one sits in the queue or, worse, gets a rubber-stamp approval that ships a bug to production.
The rest of this article is three rules you can apply to your own diff before a human ever sees it.
Rule 1: Keep the Diff Small
The single biggest predictor of review quality is diff size. A reviewer will genuinely read a 40-line change. Faced with a 400-line one, they skim it, trust you, and approve - because thoroughly reviewing it would take an hour they don’t have.
Here’s the kind of change that gets rubber-stamped: one giant handler that does everything at once.
// upload.go (400 lines)
func HandleUpload(
w http.ResponseWriter,
r *http.Request,
) {
data, _ := io.ReadAll(r.Body)
// ... 40 lines of setup
db, _ := sql.Open("pg", connStr)
rows, _ := db.Query(selectSQL)
resp := buildResponse(rows)
json.NewEncoder(w).Encode(resp)
// ... 300 more lines
}
Now compare that to a change scoped to one thing, with errors actually handled:
// upload.go (8 lines)
func HandleUpload(
w http.ResponseWriter,
r *http.Request,
) {
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad", 400)
return
}
save(data)
}
The point isn’t that every function must be eight lines - it’s that every pull request should do one reviewable thing. Splitting the 400-line change into a handful of small, focused PRs means each one can be understood in isolation, reasoned about, and reverted cleanly if it misbehaves. Slow is smooth, and smooth is fast: small changes are the ones that land in production and stay there, because they’re the ones a reviewer could actually verify.
Rule 2: Attack Your Own Code
A review that only confirms the code looks fine isn’t a review - it’s a formality. The job of a review is adversarial: hunt for the bug, the dead code, the over-engineering, and especially the unvalidated path you’d have waved straight through.
Here’s a function that passed a casual read and shouldn’t have:
func readConfig(path string) []byte {
// no validation - review missed this
data, _ := os.ReadFile(path)
return data
}
It looks harmless. But path is attacker-influenced, and reading it directly is a classic path-traversal vulnerability - feed it ../../etc/passwd and the function happily hands it back. An adversarial review catches it and forces validation before the read:
func readConfig(path string) []byte {
// review caught it: validate first
clean, err := safe(path)
if err != nil {
return nil
}
data, _ := os.ReadFile(clean)
return data
}
The fix is small; the discipline that produces it is the valuable part. Treat every new input as untrusted and ask “how would I break this?” before a reviewer has to. This is the same mindset we use when hardening an MCP server in Go - the danger is always the path you assumed was safe.
Rule 3: No Observability, No Merge
The last rule is the one most teams skip: if you add a new code path, it ships with the logs, metrics, and traces needed to tell you what it’s doing at 3am. If you can’t see it, you can’t trust it - and neither can your reviewer.
Here’s a handler that works in the happy path and tells you nothing when it doesn’t:
func handleEvent(
ctx context.Context,
e Event,
) error {
result, err := process(ctx, e)
if err != nil {
return err
}
return store(ctx, result)
}
When this starts failing in production, you have a returned error and no idea which events, how often, or why. Now the observable version:
func handleEvent(
ctx context.Context,
e Event,
) error {
log.Info("handling event",
slog.String("type", e.Type),
slog.String("id", e.ID),
)
result, err := process(ctx, e)
if err != nil {
metrics.EventErrors.Inc()
return err
}
metrics.EventsProcessed.Inc()
return store(ctx, result)
}
The structured log records exactly which event we’re handling, and the two counters give you an error rate you can alert on. Notice the log uses slog structured fields rather than a formatted string - that’s deliberate, because structured logs are queryable in a way that fmt.Sprintf lines never are. Making observability part of your “definition of done” means a reviewer can approve a change knowing they’ll be able to debug it later, instead of finding out the hard way that the new path is a black box.
Running an Adversarial Review Before You Open the PR
You don’t have to do all of this by eye. Run the rules as an automated pass over your branch first. With Claude Code, /code-review ultra kicks off a deep, multi-agent adversarial review of your current branch - it scopes the diff, hunts for the bug and the unvalidated path, and flags missing observability, all before a human ever opens the PR.
The win is the same either way: your reviewer opens a clean, tight, observable diff and merges it in minutes. Be the person whose pull requests are a joy to review.
Frequently Asked Questions
Why are small pull requests easier to review?
Reviewers have limited attention, and review quality drops sharply as diff size grows. A small PR can be read in full, reasoned about, and reverted cleanly if it breaks. A large one gets skimmed and rubber-stamped, which is how bugs reach production with an approval attached.
How do I make a large change into smaller pull requests?
Split by responsibility, not by file. Land the refactor, the new data model, and the new behaviour as separate PRs that each do one thing and keep the build green. Feature flags let you merge incomplete work safely so you can ship in small increments instead of one giant drop.
What is an adversarial code review?
An adversarial review actively tries to break the code rather than confirm it looks fine. You hunt for the bug, the dead code, the over-engineering, and especially untrusted inputs that aren’t validated - like the path-traversal example above. The goal is to find the problem yourself before a reviewer or an attacker does.
What observability should a new code path include?
At a minimum, a structured log line identifying what’s being handled, a metric for success and failure counts, and a trace span if the path crosses service boundaries. Use structured fields (such as Go’s slog) rather than formatted strings so the data is queryable. The test is simple: could you debug this path at 3am from telemetry alone?
Can AI review my pull request before a human does?
Yes. Tools like Claude Code’s /code-review run an automated review over your branch and surface bugs, security issues, and missing observability before you open the PR. It doesn’t replace human review - it makes the human review faster by handing the reviewer a diff that’s already been attacked.
Does keeping diffs small slow down delivery?
No - it speeds it up. Small changes merge faster because they’re faster to review, and they’re safer to deploy because they’re easy to reason about and revert. “Slow is smooth, and smooth is fast”: the small changes are the ones that land in production and stay there.
What’s Next
- Tighten your branching and PR habits with Getting Started With Git
- Automate the rules in CI with Getting Started With GitHub Actions
- See adversarial review in action against real code in MCP Server Security in Go
Share this article
Continue Learning
A Few of a Senior Software Engineer's Top Priorities
The most important concepts you should possibly consider as a senior software engineer.
Onion Architecture 🧅
How to utilise Onion Architecture in your applications.
Designing a Production Grade REST API
In this tutorial, we are going to look at what it takes to design a production-ready REST API.
Software Engineering
Explore all of our Software Engineering Articles