Senior 6 min · March 06, 2026

Code Review Best Practices — Catching the Bugs Tests Miss

An integer truncation bug shipped because no reviewer asked 'what if under $10?' Build a review process that catches arithmetic edge cases.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Code review is the last human checkpoint before code ships — catch logic bugs, not style issues
  • Review in priority order: correctness → security → design → style (linters handle style)
  • Every comment needs a label: [blocking], [suggestion], [nit] — ends guessing games
  • Effectiveness drops sharply above 200-300 lines (Microsoft Research data)
  • Biggest mistake: rubber-stamping a PR you don't fully understand
Plain-English First

Imagine you just wrote an essay for school and your friend reads it before you hand it in. They catch the spelling mistake you missed, point out the paragraph that confuses them, and suggest a clearer ending. That's a code review — a second pair of eyes on your work before it ships to real users. The goal isn't to prove someone made a mistake. It's to catch problems cheaply, before they cost your company a 3am incident.

Every bug that reaches production was once sitting in a pull request, waiting to be caught. Code review is the last checkpoint between a developer's local machine and real users — and most teams treat it like a rubber stamp. A reviewer skims 400 lines, leaves a few nit-picks about naming, clicks 'approve', and calls it done. Meanwhile a subtle off-by-one error in the billing logic ships on Friday afternoon and quietly charges customers twice all weekend. Code review done right prevents that.

The problem isn't that developers don't care. It's that nobody explicitly teaches how to review code. Syntax is taught. Algorithms are taught. But the craft of reading someone else's code critically, giving feedback that improves the code without crushing the author, and knowing which battles to fight? That's learned the hard way — usually after a production incident that a good review would have caught. Code review is also where junior developers level up fastest, because reading production-grade code written by seniors and asking 'why did they do it this way?' accelerates learning by years.

After reading this article you'll know how to structure a review so you catch real bugs — not just style issues. You'll know what to look for at each stage of a review, how to write feedback that people actually act on, and how to build a team culture where code review is a conversation, not a judgment. There are annotated examples, a checklist you can use tomorrow, and the exact mistakes that turn good developers into reviewers nobody wants to work with.

What You're Actually Looking For: The Review Hierarchy

Most reviewers jump straight to style: variable names, formatting, missing semicolons. That's backwards. Style issues are the least important thing in a code review, and they're also the easiest to automate away with a linter. Reviewing style manually is wasting a human brain on a job a machine does better.

Think of code review as a hierarchy. At the top — most important — is correctness. Does the code do what it claims to do? Are there edge cases where it silently fails? Below that is security. Does this code open an injection vector, log a password, or trust user input it shouldn't? Then comes design: is this change the right solution, or is it the right code for the wrong problem? Only at the bottom of the hierarchy do things like naming and formatting belong.

When you open a pull request, resist the urge to start at line one and read top to bottom. Instead, read the PR description first. Understand the intent. Then look at the tests — they tell you what the author thought the contract was. Then read the code against that contract. You're asking: 'Does this code actually deliver what the description and tests promise, and are there cases the author didn't consider?'

OrderDiscountService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
 * A realistic example of the kind of bug a good code reviewer should catch.
 * The author's intent is clear. The tests might even pass. But there's a
 * correctness bug hiding in plain sight.
 */
public class OrderDiscountService {

    private static final double LOYALTY_DISCOUNT_RATE = 0.10;  // 10% for loyal customers
    private static final int    LOYALTY_THRESHOLD_DAYS = 365;  // 1 year to qualify

    /**
     * Calculates the final price after applying a loyalty discount.
     * BUG: uses integer division — if orderTotal is less than 10,
     * the discount rounds down to 0, silently giving no discount at all.
     */
    public double calculateLoyaltyPrice(double orderTotal, int customerAgeDays) {

        if (customerAgeDays < LOYALTY_THRESHOLD_DAYS) {
            // Customer hasn't been with us a year yet — no discount
            return orderTotal;
        }

        // REVIEWER SHOULD CATCH THIS: casting to int before multiplying
        // truncates the discount for small orders, e.g. $9.99 * 0.10 = $0.999,
        // cast to int = 0, so discount becomes $0.00
        int discountAmount = (int) (orderTotal * LOYALTY_DISCOUNT_RATE);

        return orderTotal - discountAmount;
    }

    /**
     * FIXED version a reviewer would suggest:
     * Keep the calculation in floating point throughout.
     */
    public double calculateLoyaltyPriceFixed(double orderTotal, int customerAgeDays) {

        if (customerAgeDays < LOYALTY_THRESHOLD_DAYS) {
            return orderTotal;
        }

        // Stay in double — no silent truncation
        double discountAmount = orderTotal * LOYALTY_DISCOUNT_RATE;

        return orderTotal - discountAmount;
    }

    public static void main(String[] args) {

        OrderDiscountService service = new OrderDiscountService();

        double smallOrder = 9.50;   // A customer buys a $9.50 item
        int loyalCustomerAge = 400; // They've been with us 400 days — qualifies

        double buggyResult = service.calculateLoyaltyPrice(smallOrder, loyalCustomerAge);
        double fixedResult  = service.calculateLoyaltyPriceFixed(smallOrder, loyalCustomerAge);

        // Show the reviewer the cost of missing this bug
        System.out.println("=== Loyalty Discount Comparison ===");
        System.out.printf("Order total:          $%.2f%n", smallOrder);
        System.out.printf("Buggy price (int):    $%.2f  <-- customer overcharged!%n", buggyResult);
        System.out.printf("Correct price (double):$%.2f  <-- proper 10%% discount%n", fixedResult);
    }
}
Output
=== Loyalty Discount Comparison ===
Order total: $9.50
Buggy price (int): $9.50 <-- customer overcharged!
Correct price (double):$8.55 <-- proper 10% discount
Watch Out: Style Creep
If more than 30% of your review comments are about formatting or naming rather than logic, your linter isn't configured properly. Set up Checkstyle, ESLint, or Prettier to auto-enforce style in CI — then every review comment you write is about something a machine couldn't catch.
Production Insight
A common failure: teams that review style manually still ship logic bugs.
Rule: style is for machines, logic is for humans.
If your linter is correctly configured, style comments should drop to near zero.
Key Takeaway
Review in priority order: correctness first.
Style last — and only if the linter missed it.
Most bugs are in logic, not variable names.

Writing Feedback That Gets Applied, Not Ignored

The quality of your review is not just about what you find — it's about how you say it. A comment that reads 'this is wrong' gets ignored or causes defensiveness. A comment that explains the problem, why it matters, and suggests a path forward gets applied in 20 minutes.

There's a practical framework called NVC (Non-Violent Communication) that engineers have quietly borrowed for code review. The format is: observation, impact, suggestion. 'This loop iterates over all orders on every request' (observation) — 'which will be slow at scale once we have more than a few thousand orders' (impact) — 'could we filter at the database level before returning results?' (suggestion). That one comment teaches, respects the author, and gives them a direction.

Also get in the habit of labelling your comments. A comment that's a blocking correctness issue looks identical in GitHub to a comment that's a minor suggestion you'd apply if you were writing it. Labels like [blocking], [suggestion], [nit], and [question] let the author know what to prioritize and what they can skip without needing to read your mind.

Finally — leave positive comments too. When you see a clever solution to a hard problem, say so. 'Nice use of the strategy pattern here, this is going to make adding new payment providers much easier' takes five seconds to write and builds the kind of trust that makes authors actually want your reviews.

ReviewCommentExamples.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
 * This file simulates a real scenario: a UserRepository that a reviewer
 * would flag for a performance problem. The comments below demonstrate
 * the difference between a BAD review comment and a GOOD review comment
 * on the exact same issue.
 */
import java.util.List;
import java.util.ArrayList;

public class ReviewCommentExamples {

    // Simulated in-memory 'database' of users
    static List<String> allUsersInDatabase = new ArrayList<>();

    static {
        // Pre-populate with 10,000 fake users to illustrate the scale problem
        for (int i = 0; i < 10_000; i++) {
            allUsersInDatabase.add("user_" + i + "@example.com");
        }
    }

    /**
     * ORIGINAL CODE (the author's PR):
     * Finds active users whose email matches a search term.
     *
     * BAD review comment: "This is inefficient."
     *   — Why it fails: gives no context, no suggestion, sounds like a criticism of the person
     *
     * GOOD review comment:
     *   [blocking] This loads all 10,000+ users into memory on every search request
     *   (observation). At our current growth rate we'll hit OOM errors within a month
     *   (impact). Could we push the LIKE filter into the SQL query so only matching
     *   rows are fetched? Something like: SELECT * FROM users WHERE email LIKE ?
     *   (suggestion). Happy to pair on this if the query gets complex.
     */
    public static List<String> findUsersByEmailTerm_Original(String searchTerm) {

        // Loads ALL users then filters in Java — bad at scale
        List<String> allUsers = allUsersInDatabase; // imagine this is a DB call
        List<String> matchingUsers = new ArrayList<>();

        for (String email : allUsers) {
            if (email.contains(searchTerm)) { // filtering in application memory
                matchingUsers.add(email);
            }
        }
        return matchingUsers;
    }

    /**
     * IMPROVED CODE (what the reviewer's suggestion leads to):
     * In a real app the filter moves to SQL. Here we simulate that by
     * treating it as a pre-filtered list arriving from the DB layer.
     */
    public static List<String> findUsersByEmailTerm_Improved(String searchTerm) {

        // Simulates: SELECT email FROM users WHERE email LIKE '%searchTerm%'
        // Only matching rows cross the network — memory stays flat
        List<String> dbFilteredResults = simulateDatabaseQuery(searchTerm);

        return dbFilteredResults; // no redundant in-memory loop needed
    }

    /** Simulates a parameterized DB query that filters server-side */
    private static List<String> simulateDatabaseQuery(String searchTerm) {
        List<String> results = new ArrayList<>();
        for (String email : allUsersInDatabase) {
            if (email.contains(searchTerm)) {
                results.add(email);
            }
        }
        return results;
    }

    public static void main(String[] args) {

        String testSearch = "user_42";

        long startOriginal = System.nanoTime();
        List<String> originalResult = findUsersByEmailTerm_Original(testSearch);
        long durationOriginal = System.nanoTime() - startOriginal;

        long startImproved = System.nanoTime();
        List<String> improvedResult = findUsersByEmailTerm_Improved(testSearch);
        long durationImproved = System.nanoTime() - startImproved;

        System.out.println("Results match: " + originalResult.equals(improvedResult));
        System.out.printf("Original approach: %,d ns (all users loaded into memory)%n", durationOriginal);
        System.out.printf("Improved approach: %,d ns (only matched rows)%n", durationImproved);
        System.out.println("\nComment style matters as much as the finding itself.");
    }
}
Output
Results match: true
Original approach: 1,243,800 ns (all users loaded into memory)
Improved approach: 892,100 ns (only matched rows)
Comment style matters as much as the finding itself.
Pro Tip: The Label System
Prefix every comment with [blocking], [suggestion], or [nit]. It takes two seconds and eliminates the single biggest source of PR friction: the author not knowing what must be fixed vs. what's optional. Teams that adopt this report 40% faster PR turnaround.
Production Insight
The worst review feedback is the one that gets ignored.
If you write a vague comment that the author doesn't understand, you've wasted everyone's time.
Rule: every comment must be actionable. If it doesn't tell the author what to do, delete it.
Key Takeaway
Structure: observation → impact → suggestion.
Label: [blocking] [suggestion] [nit].
Positive comments build trust; trust gets your feedback applied.

The Reviewer's Checklist: What to Look for Before You Approve

A mental checklist prevents the human tendency to declare a review 'done' after finding the first interesting issue. Once your brain spots something, it relaxes — but the second bug is usually still in there. Professional reviewers work through a deliberate checklist every time.

Start with security. Any PR that touches authentication, authorization, file I/O, or database queries deserves extra time. Look for raw string concatenation in queries (SQL injection), unsanitized values in HTML templates (XSS), and hardcoded secrets. These aren't hypothetical — they appear in real PRs every week.

Next, check error handling. Production code fails in ways that local development never reveals. Is every exception either handled gracefully or deliberately re-thrown? Does the code fail open (insecure default) or fail closed? If a downstream service is unreachable, what does the user see?

Then check for test coverage on the specific scenarios you're worried about. Not overall coverage percentage — that metric is routinely gamed. Ask: 'is there a test that would have caught the bug I just spotted?' If not, that's a blocking comment.

Finally, look at the PR size. A PR over 400 changed lines is almost unreviable. Studies from Microsoft Research show that review effectiveness drops sharply above 200-300 lines. If a PR is massive, it's fair — and kind — to ask the author to split it.

SecurityReviewChecklist.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * This file demonstrates two security issues a reviewer MUST catch:
 * 1. SQL injection via string concatenation
 * 2. A sensitive value being logged
 *
 * Both patterns appear in real PRs. Both are easy to fix once spotted.
 */
public class SecurityReviewChecklist {

    private static final String DB_URL = "jdbc:h2:mem:testdb";

    /**
     * VULNERABLE: builds SQL by concatenating user-supplied input.
     *
     * REVIEWER COMMENT: [blocking][security]
     * Concatenating `username` directly into the query string allows SQL injection.
     * A username of:  ' OR '1'='1
     * ...turns this into: SELECT * FROM users WHERE username = '' OR '1'='1'
     * which returns every user in the table.
     * Fix: use a PreparedStatement with a parameterized query (see below).
     */
    public static ResultSet findUser_Vulnerable(Connection conn, String username)
            throws SQLException {

        // NEVER do this — user controls the query structure
        String rawQuery = "SELECT * FROM users WHERE username = '" + username + "'";

        // Also: logging the raw username leaks PII to your log aggregator
        System.out.println("[INSECURE LOG] Executing query: " + rawQuery);

        return conn.createStatement().executeQuery(rawQuery);
    }

    /**
     * SAFE: uses a PreparedStatement — the DB treats the parameter as
     * data, never as executable SQL, regardless of what it contains.
     */
    public static ResultSet findUser_Safe(Connection conn, String username)
            throws SQLException {

        // ? is a bind parameter — the DB driver handles escaping
        String safeQuery = "SELECT id, username FROM users WHERE username = ?";

        PreparedStatement statement = conn.prepareStatement(safeQuery);
        statement.setString(1, username); // bind user input as data, not SQL

        // Log intent without exposing the actual input value
        System.out.println("[SAFE LOG] Querying users table with parameterized lookup");

        return statement.executeQuery();
    }

    public static void main(String[] args) throws Exception {

        // Set up a lightweight in-memory DB to demonstrate the concepts
        Connection connection = DriverManager.getConnection(DB_URL, "sa", "");
        connection.createStatement().execute(
            "CREATE TABLE users (id INT PRIMARY KEY, username VARCHAR(100))"
        );
        connection.createStatement().execute(
            "INSERT INTO users VALUES (1, 'alice'), (2, 'bob')"
        );

        String maliciousInput = "' OR '1'='1";

        System.out.println("=== Security Review Demo ===");
        System.out.println("\n-- Vulnerable approach (do NOT approve) --");
        try {
            ResultSet rs = findUser_Vulnerable(connection, maliciousInput);
            int rowCount = 0;
            while (rs.next()) rowCount++;
            System.out.println("Rows returned by injection attack: " + rowCount + " (expected: 0)");
        } catch (SQLException e) {
            System.out.println("Query error: " + e.getMessage());
        }

        System.out.println("\n-- Safe approach (parameterized) --");
        ResultSet safeRs = findUser_Safe(connection, maliciousInput);
        int safeRowCount = 0;
        while (safeRs.next()) safeRowCount++;
        System.out.println("Rows returned with parameterized query: " + safeRowCount + " (correct)");

        connection.close();
    }
}
Output
=== Security Review Demo ===
-- Vulnerable approach (do NOT approve) --
[INSECURE LOG] Executing query: SELECT * FROM users WHERE username = '' OR '1'='1'
Rows returned by injection attack: 2 (expected: 0)
-- Safe approach (parameterized) --
[SAFE LOG] Querying users table with parameterized lookup
Rows returned with parameterized query: 0 (correct)
Interview Gold: The 200-Line Rule
Microsoft Research published data showing reviewers find ~70% of bugs in the first 200 lines, and effectiveness drops steeply after that. If an interviewer asks 'what makes a good PR?', mentioning PR size limits backed by this research will stand out from every candidate who just says 'good commit messages'.
Production Insight
A team that only checks for style will ship a SQL injection.
A team that follows a checklist will catch it before merge.
Rule: a checklist turns 'I'll remember' into 'I never forget'.
Key Takeaway
Checklist: security → error handling → tests → size.
Don't approve a PR that could cause a security incident.
The 200-line rule: if you can't review it in 20 minutes, it's too big.

Building a Team Code Review Culture That Doesn't Burn People Out

The technical side of code review is the easy part. The hard part is making it sustainable. Teams burn out on reviews when PRs sit for days, when reviewers write essays on every PR, or when every review feels like a performance evaluation.

Three process habits fix most of this. First, set a response SLA. Agree as a team that a PR gets a first review within one business day. This removes the uncertainty that causes authors to chase reviewers on Slack. Most review delays aren't about workload — they're about nobody knowing whose job it is right now.

Second, separate author and reviewer responsibilities clearly. The author is responsible for writing a clear PR description that explains the what and why, linking to the relevant ticket, and keeping the PR small enough to review. The reviewer is responsible for timely feedback, clear labels, and approving once blocking issues are resolved — not perfecting the code to their personal standard.

Third, rotate reviewers deliberately. If the same senior engineer reviews every PR, you get a bottleneck and you deprive junior engineers of the learning opportunity. Junior engineers reviewing senior code is valuable — they ask 'why?' questions that expose implicit assumptions that should be documented. Build a rotation so everyone reviews and everyone gets reviewed.

PullRequestDescriptionTemplate.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
 * This is not runnable code — it's a PR description template formatted as
 * a Java comment block to fit the article format.
 *
 * A good PR description is half the battle. Reviewers approve faster
 * when they understand intent before they read a single line of code.
 *
 * ─────────────────────────────────────────────
 * WHAT MAKES A GREAT PR DESCRIPTION (Template)
 * ─────────────────────────────────────────────
 *
 * ## What does this PR do?
 * Adds a loyalty discount calculation to the order checkout flow.
 * Customers who have been registered for 365+ days receive 10% off.
 *
 * ## Why are we doing this?
 * Addresses customer retention feedback from Q3 survey (Ticket: RET-441).
 * Loyalty customers were churning because we had no visible benefit to staying.
 *
 * ## How does it work?
 * - New `OrderDiscountService` calculates discount based on account age
 * - Discount applied in `CheckoutController` before payment is initiated
 * - Rate and threshold are config-driven (see application.yml change)
 *
 * ## What should the reviewer focus on?
 * - The discount calculation in `calculateLoyaltyPrice()` — I want a sanity
 *   check that the rounding behaviour is correct for small order values
 * - The new index on `users.created_at` in the migration — is this the
 *   right column to index or should it be a composite index?
 *
 * ## Testing
 * - Unit tests cover: no discount below threshold, exact threshold day,
 *   above threshold, zero-value order, max double order value
 * - Manual test: created a test account dated 2 years ago, confirmed
 *   10% applied correctly at checkout
 *
 * ## What's NOT in this PR?
 * Email notification to loyalty customers is out of scope — tracked in RET-445.
 * ─────────────────────────────────────────────
 */
public class PullRequestDescriptionTemplate {

    public static void main(String[] args) {
        System.out.println("A good PR description answers 5 questions:");
        System.out.println("1. WHAT:  What does this change?");
        System.out.println("2. WHY:   What business/technical reason?");
        System.out.println("3. HOW:   High-level approach (not line-by-line)");
        System.out.println("4. FOCUS: What do you specifically want eyes on?");
        System.out.println("5. SCOPE: What is deliberately NOT included?");
        System.out.println();
        System.out.println("Reviewers with good context approve 60%+ faster.");
        System.out.println("Context reduces nit-picks and increases real bug finds.");
    }
}
Output
A good PR description answers 5 questions:
1. WHAT: What does this change?
2. WHY: What business/technical reason?
3. HOW: High-level approach (not line-by-line)
4. FOCUS: What do you specifically want eyes on?
5. SCOPE: What is deliberately NOT included?
Reviewers with good context approve 60%+ faster.
Context reduces nit-picks and increases real bug finds.
Pro Tip: The Author's Pre-Review
Before you open a PR, review your own diff as if you were a stranger seeing this code for the first time. You'll catch 30-40% of your own issues — wrong variable names, missing null checks, leftover debug logs — before the reviewer ever sees them. It also forces you to write a better description because you'll notice where your own intent is unclear.
Production Insight
Teams without SLA burn out: authors get blocked, reviewers feel guilty.
The fix is simple: agree on 'first response within 1 business day'.
Rule: a process that works for the whole team beats a process that only works for the most senior person.
Key Takeaway
Set an SLA. Rotate reviewers. Define responsibilities.
The goal is sustainable, not perfect.
A good PR description saves an hour of back-and-forth.

Code Review as a Learning Tool: Reviews That Level Up the Whole Team

Code review isn't just a quality gate — it's one of the most powerful learning mechanisms in software engineering. When done right, every review teaches both the author and the reviewer something they didn't know before.

For junior developers, reading senior code is like getting a guided tour of production patterns. They see how error handling is structured, how configurations are externalised, how tests are written for edge cases they never thought of. The comment 'why did you use a factory here instead of a constructor?' is worth a hundred tutorials because it's anchored to real code their team owns.

For senior developers, reviewing junior code forces them to articulate tacit knowledge. Explaining why a certain approach is preferred, or why a race condition exists, makes the senior a better teacher and often reveals gaps in their own understanding. It's the 'explaining forces understanding' effect — if you can't explain why something should be done a different way, maybe you don't understand it well enough yourself.

To make this work, treat code review as a conversation, not an audit. Leave 'why' questions alongside 'fix this' comments. When you see something surprising, ask. The answer might be a design decision you hadn't considered — or it might be a gap in the PR that leads to a better solution. Either way, everyone learns.

LearningOpportunityExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.List;

/**
 * Demonstrates a junior-vs-senior code review moment.
 * The junior wrote a method that works but is fragile.
 * The senior's comment teaches a better pattern.
 */
public class LearningOpportunityExample {

    // Junior's original code:
    public static String formatUserName(String firstName, String lastName) {
        // What if both are null? What if only one is null?
        return firstName + " " + lastName;  // null + " " + null = "null null"
    }

    // Senior's review comment:
    // [blocking] If either name is null, this returns "null LastName" or "FirstName null".
    // Better to use a helper that handles nulls properly.
    public static String formatUserNameSafe(String firstName, String lastName) {
        // Using a null-safe approach:
        return List.of(firstName, lastName)
            .stream()
            .filter(s -> s != null && !s.isBlank())
            .collect(java.util.stream.Collectors.joining(" "));
    }

    public static void main(String[] args) {
        System.out.println("Fragile: '" + formatUserName(null, "Doe") + "'");
        System.out.println("Safe: '" + formatUserNameSafe(null, "Doe") + "'");
    }
}
Output
Fragile: 'null Doe'
Safe: 'Doe'
Code Review as Teaching
  • Juniors learn patterns by reading senior code — 'why did you use a factory here?' is a powerful question.
  • Seniors crystallise their own understanding by explaining decisions to juniors.
  • Treat code review as a conversation, not an audit. Ask 'why' as well as 'fix this'.
  • A comment that teaches is worth ten that merely demand a change.
Production Insight
Teams that treat reviews as teaching tools have faster onboarding and fewer recurring bugs.
The teachable moment is lost if the reviewer just says 'fix it' without explaining why.
Rule: if you can't explain why it's wrong, you don't understand the problem well enough.
Key Takeaway
Code review is a learning mechanism.
Explain why, not just what.
A question that teaches is better than a command that fixes.
● Production incidentPOST-MORTEMseverity: high

When the Reviewer Missed the Integer Division Bug

Symptom
Customers with orders under $10 who qualified for a 10% loyalty discount were charged full price. The bug was only discovered when a user tweeted a screenshot of their receipt.
Assumption
The logic was correct because test coverage was 85% and all existing tests passed.
Root cause
The discount calculation cast orderTotal * LOYALTY_DISCOUNT_RATE to int before subtracting from the total. For any order under $10, the discount became $0.00 due to integer truncation.
Fix
Change the discount variable type from int to double. Add a unit test for small-order edge cases. Add a code review checklist item: 'Check arithmetic for truncation errors.'
Key lesson
  • Never trust a PR that passes all tests but has a logical edge case uncovered.
  • Your code review checklist must include arithmetic edge cases — especially rounding and casting.
  • If a reviewer hasn't asked 'what happens if this value is less than X?' they haven't finished reviewing.
Production debug guideSymptom-based troubleshooting for teams whose code reviews aren't catching bugs5 entries
Symptom · 01
PRs sit unreviewed for 3+ days
Fix
Agree on a team SLA: first review within 1 business day. Use Slack reminders or a 'review queue' board. If someone is blocked, they escalate to the tech lead.
Symptom · 02
80% of comments are about formatting
Fix
Configure a linter (ESLint, Prettier, Checkstyle) to run in CI and block the PR on style violations. Then ban all style comments in human review.
Symptom · 03
Authors ignore review comments or push back defensively
Fix
Adopt labelled comments: [blocking], [suggestion], [nit]. This removes ambiguity. Hold a team session to set expectations that blocking comments must be addressed, suggestions are optional.
Symptom · 04
Senior engineer reviews every PR alone
Fix
Rotate reviewers. Juniors must review senior code. Seniors must review junior code. Use a rota or a tool that enforces distribution.
Symptom · 05
PR descriptions are just a link to a Jira ticket
Fix
Require a PR description template: WHAT, WHY, HOW, FOCUS, SCOPE. Enforce it via CI (e.g., a GitHub action that checks description length).
★ Common Code Review Pitfalls Cheat SheetUse this when you're reviewing and want to make sure you don't miss the real issues. Print it, pin it, use it every time.
I don't understand the code's intent
Immediate action
Read the PR description. If it's missing, ask the author to clarify the WHAT and WHY before continuing.
Commands
git log --oneline -5 (check if commit messages are descriptive)
Review the test file first — it reveals the expected contract.
Fix now
Write a comment: [question] I can't understand why this code exists. Could you add a 2-line explanation?
The PR is bigger than 400 lines+
Immediate action
Stop reviewing. Ask the author to split the PR into smaller logical chunks. Studies show review effectiveness drops above 200-300 lines.
Commands
Count changed lines in GitHub: diff stats
Ask: 'Can you split this into 2-3 PRs that are independently deployable?'
Fix now
Block the review: [blocking] This PR is too large for effective review. Please split into xxx and yyy.
I can't tell if a comment is mandatory or optional+
Immediate action
Label all your comments. If the author seems confused, they probably can't read your intent. Adopt [blocking], [suggestion], [nit] prefixes today.
Commands
Search your own comment history for 'this is wrong' and replace with structured feedback.
Pair with the author on one of your blocking comments to show how to fix it.
Fix now
Add a comment at the top of the review: 'All my comments are labelled: [blocking] must fix, [suggestion] optional, [nit] extremely optional.'
Effective vs Ineffective Code Review
AspectIneffective Code ReviewEffective Code Review
Primary focusStyle and formattingCorrectness, security, and design
Comment formatVague: 'this is wrong'Labelled with observation + impact + suggestion
PR size500-1000+ lines — hard to reviewUnder 400 lines, ideally under 200
Review triggerWhenever the reviewer gets around to itWithin 1 business day SLA agreed by team
Who reviewsOnly the most senior engineerRotated across the team including juniors
Blocking vs. optionalUnclear — author guessesExplicitly labelled [blocking], [nit], [suggestion]
PR descriptionLink to Jira ticket, nothing elseWhat, why, how, focus areas, and scope
Tests checkedOverall coverage % glanced atSpecific tests for each logic branch in the PR
Security checkOnly if the reviewer remembersChecklist: queries, auth, logging, user input
Positive feedbackRare or neverExplicitly given when good work is spotted

Key takeaways

1
Review in priority order
correctness first, then security, then design, then style — style is at the bottom because a linter should be catching it automatically
2
Label every review comment as [blocking], [suggestion], or [nit]
this single habit eliminates the ambiguity that causes most PR friction and delays
3
A PR description that answers WHAT, WHY, HOW, FOCUS, and SCOPE makes reviewers 60% faster and dramatically increases the quality of feedback they give
4
Review effectiveness drops sharply above 200-300 lines (per Microsoft Research)
splitting large PRs isn't bureaucracy, it's the only way to get real reviews instead of rubber stamps
5
Code review is a teaching tool, not just a gate
every review is a chance to level up both the author and the reviewer

Common mistakes to avoid

3 patterns
×

Reviewing style instead of substance

Symptom
80% of your comments are about variable names, spacing, or import order, and the PR still ships with a logic bug
Fix
Configure a linter (Checkstyle, ESLint, Prettier) to block the PR on style issues automatically, then spend every human review minute on correctness, security, and design instead
×

Approving a PR you don't understand

Symptom
You can't explain what a function does after reading it, but you click approve anyway to avoid slowing the team down
Fix
'I don't understand what this does' is a valid and important review comment; if the code isn't clear to a competent reviewer, it's either too complex or missing documentation, and both are real bugs
×

Making every comment blocking when most aren't

Symptom
Authors spend hours fixing nits and suggestions they thought were optional, causing resentment and slower PRs
Fix
Label every comment as [blocking] (must fix before merge), [suggestion] (take it or leave it), or [nit] (tiny thing, not worth holding the PR); this single habit eliminates most review friction overnight
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Walk me through how you approach reviewing a pull request — what do you ...
Q02SENIOR
Tell me about a time a code review caught a bug that would have been exp...
Q03SENIOR
If a senior engineer on your team consistently writes PRs that are 800+ ...
Q01 of 03SENIOR

Walk me through how you approach reviewing a pull request — what do you look for first, second, and last, and why in that order?

ANSWER
First I read the PR description to understand the intent. Then I look at the tests to see what contract the author intended. Then I read the code, checking correctness and edge cases. I look for security issues next (injection, auth flaws), then error handling, then design. Style I ignore unless the linter missed something. The order matters because the most expensive bugs are logic and security issues — style is cheap to fix later.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How long should a code review take?
02
Should junior developers review senior developers' code?
03
What's the difference between a code review and a pair programming session?
04
How do I handle a reviewer who is consistently rude or dismissive?
05
What if I'm the only reviewer and the code is completely new to me?
🔥

That's Software Engineering. Mark it forged?

6 min read · try the examples if you haven't

Previous
Clean Code Principles
6 / 16 · Software Engineering
Next
Test-Driven Development — TDD