Home → Engineering
Code Review: Spreading Knowledge, Not Guarding a Gate
The pull request sat open for three days. When it was finally reviewed it got 41 comments: 38 about formatting, 3 about naming. Not one said “this change creates a circular dependency between two services”. We learned that three months later, during an outage.
- The main job of review is spreading knowledge, not catching bugs. Catching bugs is a nice side effect; if it is the only goal, tests are cheaper.
- As a PR grows, its comments lose value. You do not get fewer comments — you get fewer useful ones.
- Label every comment: blocking, suggestion, nit, question. An unlabelled comment is the seed of a three-day argument.
- Waiting time decides quality. Review comes before your own sprint work. Otherwise the PR goes cold, the branch conflicts, and the reviewer skims.
- Not everything needs a second approval. Money, security and schema changes do. Everywhere else it only adds a day.
- AI increased the volume, so the bottleneck moved to review. What to look for in generated code is in the vibe coding post. This one is about the process.
From the field: 41 comments, 0 about architecture
That PR was 780 lines and carried three days of work in one piece. The reviewer was not a weak engineer — quite the opposite, they were the most careful person on the team. But when you open 780 lines, the brain stops reading and starts scanning. And when you scan, you write down whatever catches your eye: whitespace, variable names, a missing log line.
Architectural objections do not need attention. They need context. To say “this service should not call that one directly” you have to hold the relationship between two services in your head, and by line 400 of 780 that space is already full.
My mistake was this: instead of telling the author to split the PR, I told the reviewer to look more carefully. I was pushing on the wrong side. After the outage three months later we changed the rule: splitting is the author’s job. Attention is not the reviewer’s job.
The inverse relationship between size and value
When we grouped three months of pull requests by size, this is what came out:
| PR size | Average comments | Architecture / logic comments | Time spent reviewing |
|---|---|---|---|
| Under 100 lines | 4 | Most of them | 8 minutes |
| 100–400 lines | 11 | About half | 22 minutes |
| 400–800 lines | 18 | A few | 26 minutes |
| Over 800 lines | 6 | Almost none | 9 minutes |
The last row is the interesting one: PRs over 800 lines get fewer comments. At that point the reviewer gives up and writes “looks good”. So a big PR does not produce more objections. It silences them completely.
Splitting is not as hard as it looks, but the author has to plan it: first the schema change (its own release), then the new code behind a closed flag, then a small PR that opens the flag. Three reviews, all of them readable.
Comment labels: the cheapest improvement
This was the single change that improved our review culture most. Every comment starts with a label:
blocking: This line keeps the payment amount as a float. We lose
cents; it has to be decimal. Must change before merge.
suggestion: This loop could be a single query. Not required now,
but if it causes trouble later, start here.
nit: The variable could be "remaining_amount" instead of "x".
Ignore this if you like, I am just saying it.
question: Why is there no retry here? I do not know, I want to learn.
(This label puts the reviewer on the learning side too.)
Two labels are especially strong. nit lets the reviewer say what they think while leaving the author free — without it, every small preference turns into a negotiation. question takes review out of one-way inspection; a junior asking a senior a question shows that the author is not the only one learning in that PR.
blocking has one rule: write in a single sentence why it blocks and what has to change. “This approach feels wrong to me” is not a blocking comment, it is a feeling. It belongs in a conversation, not in a PR.
Waiting time: the quiet quality killer
I did not see waiting time as a quality problem until we measured it. In a waiting PR three things break at once:
- The author loses context. Answering a comment that arrives two days later takes longer than writing the code did.
- The branch starts conflicting. A waiting branch is the one that conflicts most on merge, and conflict resolution produces code nobody reviewed.
- The author grows the PR. A bored person waiting adds one more thing, and the PR slides towards the last row of the table above.
Our rule is one sentence: review comes before your own sprint work. Open PRs are the first thing in the morning. It sounds inefficient — it is not: your 30 minutes unblock two days of someone else’s.
Who approves, and how many
- Money: amounts, currency, accounting
- Authentication and authorisation
- Schema changes and migrations
- Irreversible operations, data deletion
- Public API contracts
What they share: the mistake cannot be undone, or it accumulates silently.
- UI adjustments
- Logs, metrics, dashboards
- Adding tests
- Dependency upgrades (if tests pass)
- Docs and runbooks
Here a second approval does not reduce risk. It only adds a day to the queue.
Who reviews should also be decided by ownership, not by volunteering. Without defined file ownership, review always lands on the same two people and those two become the team’s bottleneck — the same boundary problem I described in the monorepo post.
The author’s responsibility
Half of review quality is decided before the PR is opened. We expect three things from the author:
- Review your own PR first. Read the whole diff before opening it. Whatever you find, nobody else has to read — this single habit visibly lowers the comment count.
- Write the “why” in the description. The code already says what you did. Write why you chose this path, which alternative you dropped, and what you deliberately left out.
- Say where you want the reviewer to look. “The risky part is this function” keeps the review from spreading thin.
What to track
| What | Why |
|---|---|
| Time from opening to first comment | More informative than total time; it shows where the bottleneck is |
| Distribution of PR sizes | If the share over 800 lines is rising, review has effectively stopped |
| Number of distinct reviewers | If it is always the same two names, knowledge is not spreading, it is funnelling |
| Share of blocking comments | Too low means review is a ceremony; too high means design is not discussed before the PR |
What did not work for me
- Putting up a review checklist. We wrote a ten-item list; two weeks later nobody looked at it. What worked was automating the list: formatting, linting and simple patterns moved into CI. People should only look at what only people can look at.
- A “everyone reviews everyone” rotation. It looked fair. In practice someone reviewing an area they did not know could only write formatting comments. Ownership plus deliberate pairing worked better: the person who needed to learn an area was added as the second reviewer there.
- Raising the approval count to two. We did it as a reflex after an outage. The bug rate did not drop; waiting time doubled. We then narrowed it to the list above.
Checklist
- Is this PR over 400 lines? If so, why could it not be split?
- Does the description say “why”, or only “what”?
- Did the author read their own diff first?
- Do my comments have labels — is this blocking or a nit?
- Does my blocking comment say what has to change?
- Does this change fall into the second-approval list?
- How many hours has this PR been waiting?
- How many different people reviewed anything in the last month?
Conclusion
Nobody made a mistake on that 780-line PR. The author finished the work, the reviewer looked carefully and wrote 41 comments. The process ran, and three months later there was still an outage. Because what we wanted from review did not match the conditions we gave it: we wanted architectural objections, and we handed over a pile that could only be scanned.
If review is designed as a gate that decides whether code passes, it mostly catches formatting. If it is designed as a channel that spreads knowledge through the team, it catches architecture — and CI catches the formatting anyway.
Measure it with one question: in last month’s reviews, how many times did someone say “I did not know that, I learned something”? If the answer is zero, your review may be running, but it is not working.