Code Review

A structured review of code changes before they merge. Done well, code review catches bugs, spreads knowledge, enforces standards, and is the most effective quality gate a team has.

A structured review of code changes before they merge. Done well, code review catches bugs, spreads knowledge, enforces standards, and is the most effective quality gate a team has. Done poorly, it's a bottleneck that slows delivery without adding value.


What Code Review Is For

Primary goals (in order of value):
  1. Bug detection — the earlier the cheaper
  2. Correctness and completeness — does it actually solve the problem?
  3. Knowledge transfer — reviewer learns, author learns from questions
  4. Design feedback — is this the right abstraction?

Secondary goals:
  5. Standards enforcement — but use linters for this, not humans
  6. Security audit — but dedicated security review is more reliable

Not the goal:
  - Style enforcement (use a formatter)
  - Pointing out things that "could be done differently" without reason
  - Demonstrating the reviewer's knowledge

Reviewer Checklist

## Correctness
- [ ] Does this solve the stated problem?
- [ ] Are there edge cases not handled? (null, empty list, concurrent access, auth failure)
- [ ] Are all paths through the code reachable and correct?
- [ ] Does it handle errors? Are errors surfaced or swallowed?

## Design
- [ ] Is the abstraction level right? (not too generic, not too specific)
- [ ] Does this introduce unexpected complexity?
- [ ] Could this be simpler without losing correctness?
- [ ] Are side effects visible and limited?

## Tests
- [ ] Are there tests for the new behaviour?
- [ ] Do the tests verify behaviour, not implementation?
- [ ] Are unhappy paths tested?
- [ ] Would a test failure tell me what broke?

## Security
- [ ] Is user input validated at the boundary?
- [ ] Are queries parameterised (no string concatenation)?
- [ ] Are there hardcoded secrets?
- [ ] Are new endpoints protected with auth?

## Observability
- [ ] Are errors logged with enough context to diagnose?
- [ ] Are key operations instrumented?

How to Give Good Feedback

Use the CARES framework:

Context:    "In a high-traffic path like this..."
Action:     "...I'd extract the DB call into a separate function..."
Reason:     "...because connection timeouts here cascade to 500s for the user."
Example:    (optional code snippet)
Suggestion: "Happy to pair on this if useful."

Grade comments:
  [blocking] — must fix before merge (bug, security issue, correctness gap)
  [non-blocking] — good to address but won't hold the PR
  [nit] — cosmetic preference, take it or leave it
  [question] — asking to understand, not requesting a change

Don't:
  "This is wrong." (no reason, no suggestion)
  "I would have done X." (unless X is clearly better)
  "Why did you do it like this?" (sounds accusatory — ask differently)

Do:
  "This will fail if `items` is empty — line 47 would panic on index 0."
  "Nit: prefer `items.is_empty()` over `len(items) == 0` — more idiomatic in Rust."
  "Question: is there a reason we're not using the existing `UserService` here?"

PR Author Responsibilities

Before requesting review:
  - Self-review your own diff first — would you approve this?
  - Tests pass locally; linter clean; type checker clean
  - PR description explains WHY, not just WHAT
  - PR is scoped: one concern, reviewable in 15-20 minutes
  - No debugging artifacts left in (print statements, commented-out code)

Good PR description:
  Problem: What user/system problem does this solve?
  Solution: High-level approach and key decisions
  Testing: What did you test and how?
  Screenshots: If it's a UI change, include before/after

PR size:
  < 400 lines changed: easy to review, most defects caught
  400-800 lines: reviewers skim, defects slip through
  > 800 lines: mostly ceremonial approval, quality benefit near zero

Review Process Design

Norms that make review work:

Turnaround SLA:
  Reviews requested → first response within 4 hours (same business day)
  Don't let PRs sit overnight unanswered

LGTM with conditions:
  "I'm approving but please address [X] before merging"
  Trusts the author, removes blocking round-trips

Draft PRs:
  Use for early design feedback without full review overhead
  Opens discussion before too much code is written

Review rounds:
  After author addresses comments, re-request review
  Reviewers should focus only on changed lines in second pass

Auto-assign reviewers:
  CODEOWNERS file — specific teams own specific paths
  Rotation rules — spread review load across team

CODEOWNERS Example

# .github/CODEOWNERS

# Global fallback — any file not matched below
*                   @org/team-leads

# Backend
src/api/            @org/backend-team
src/models/         @org/backend-team

# Frontend
src/components/     @org/frontend-team
src/pages/          @org/frontend-team

# Infrastructure — mandatory security review
infra/              @org/platform-team @org/security-team
.github/            @org/platform-team

# Critical paths — senior review required
src/payments/       @org/backend-senior
src/auth/           @org/security-team

Automated Review Aids

# .github/workflows/pr-check.yaml
name: PR Quality Gate

on: [pull_request]

jobs:
  size-check:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Check PR size
      run: |
        LINES=$(git diff origin/${{ github.base_ref }} --stat | tail -1 | grep -o '[0-9]* insertion' | grep -o '[0-9]*')
        if [ "$LINES" -gt 800 ]; then
          echo "::warning::PR has $LINES lines changed. Consider splitting."
        fi

Common Failure Cases

Review bottleneck: PRs sit for 24+ hours waiting for a first response Why: no explicit SLA exists, reviewers treat review as lower priority than their own work, and there is no automated reminder or escalation. Detect: measure the median time-to-first-review in GitHub Insights; if it exceeds 4 hours (same business day), the process is blocked. Fix: establish and publish a turnaround SLA (4 hours first response), add a Slack or GitHub Actions reminder after 4 hours with no review, and distribute load via CODEOWNERS rotation.

Rubber-stamp approvals on large PRs Why: PRs over 800 lines are cognitively too large to review thoroughly; reviewers approve quickly to unblock the author rather than audit the diff. Detect: PRs with 800+ lines changed that receive approval within 10 minutes, or approval comments with no substantive feedback. Fix: enforce a PR size gate in CI (warn at 400 lines, block at 800), and require authors to split large changes into a stack of smaller PRs before requesting review.

Blocking comments block permanently without follow-through Why: a reviewer marks a comment [blocking] but then approves on the next round without verifying the fix was actually made; the issue ships anyway. Detect: review the diff between request-changes and approval rounds — was the blocking issue addressed or just marked resolved by the author? Fix: on the second review pass, explicitly verify that each blocking comment was resolved correctly before approving; use GitHub's "re-request review" workflow to enforce another pass.

Linter and formatter issues debated in review instead of automated Why: reviewers spend comment threads on style, indentation, and import ordering — issues a pre-commit hook or CI formatter check would catch automatically. Detect: review comments containing words like "formatting", "trailing space", "import order", or "snake_case". Fix: add ruff, black, and isort to a pre-commit hook and CI gate; agree to never leave style comments in review — if it passes the formatter, it is correct.

Connections

se-hub · cs-fundamentals/clean-code · cs-fundamentals/tdd-se · qa/defect-prevention · qa/agile-qa · cs-fundamentals/oop-patterns

Open Questions

  • What are the most common misapplications of this concept in production codebases?
  • When should you explicitly choose not to use this pattern or technique?