paulund

Reviewing Pull Requests

Reviewing Pull Requests

Reviewing the code of your peers is a critical part of the software development lifecycle. It helps you ensure that the code is high quality, well-documented, and follows established practices. It is also one of the best ways to learn from your colleagues and share your own knowledge. This guide covers the practices that make code review effective and enjoyable.

1. Understand the Purpose of the Change

Before you start reviewing the code, take the time to understand what the change is trying to achieve.

  • What problem is it solving?
  • What is the expected outcome?
  • What are the success criteria?

Understanding the purpose helps you provide better feedback and spot whether the change actually meets its requirements. You may find that the technical approach outlined in the ticket is not the best one — sometimes the person who wrote the requirement did not have full visibility of the technical landscape. There may already be a configuration option or an existing feature that covers the need. Raising this early saves everyone time.

Watch for scope creep as well. A pull request that adds features beyond what was asked for introduces risk in areas of the codebase that nobody requested a change to. The change should stay as small as possible — this makes it easier to review, test, and revert if needed.

The success criteria also tell you what tests should exist. If you notice that a success criterion has no corresponding test, ask for one.

2. Design of the Code

Is the code well-structured?

Every codebase has an established structure. The new code should follow it. If it does not, it will be harder for other developers to find and understand in the future. Check whether the code applies sound engineering principles — SOLID, DRY, KISS, YAGNI — and whether it can be understood without a lengthy explanation.

Avoid over-engineering

Code that is more complex than the problem warrants is just as problematic as code that is too simple. Here is a classic example:

// Simple and clear
function add(a, b) {
  return a + b;
}
// Over-engineered — unnecessary abstraction for a trivial operation
class Addition {
  constructor(a, b) {
    this.a = a;
    this.b = b;
  }

  validate() {
    if (typeof this.a !== 'number' || typeof this.b !== 'number') {
      throw new Error('Both inputs must be numbers');
    }
  }

  calculate() {
    this.validate();
    return this.a + this.b;
  }
}

const addition = new Addition(1, 2);
console.log(addition.calculate());

If the simpler version does the job, use it. Complexity does not earn points — it earns maintenance burden.

Is the code easy to maintain?

Maintainability comes down to a few habits: small, single-responsibility files; clear naming conventions; adequate documentation; good tests; and no unexplained magic numbers or hard-coded strings.

Has testing been considered in the structure?

Code that is hard to test is code that nobody will test. Check whether the dependencies are injectable or mockable. If the code calls a third-party API, the tests must mock that call — both to keep them fast and to avoid coupling your test suite to an external service's availability.

Will this need to be extended?

If you have broader knowledge of the product roadmap, use it. A provider service that currently supports one provider will almost certainly need to support others in the future. The code should be structured to accommodate that without a rewrite — but it should not build that flexibility speculatively. Strike the balance and share your thinking with the author.

Can any of this be reused?

Sometimes developers write something highly specific to the feature at hand when a more generic version would serve multiple parts of the codebase. A Map component is more valuable than a ContactUsMap component, provided the abstraction does not obscure its purpose. Highlight these opportunities when you see them.

Will this be easy to debug?

Does the code have enough logging in place to diagnose issues in production? If you would struggle to understand what went wrong from the logs alone, ask for more instrumentation.

How will this be deployed?

Complex changes can be difficult to deploy safely. Think about whether the change can be rolled back cleanly, whether it requires a database migration, and whether it affects any other part of the deployment pipeline.

3. Code Quality

When reviewing, consider:

  • Is the change more complex than it needs to be?
  • Does the code follow the project's coding standards and best practices?
  • Is it well-documented where documentation is warranted?
  • Are there any code smells — duplicated logic, long functions, deeply nested conditionals?
  • Are there performance concerns?
  • Does it follow the project's naming conventions?
  • Are there magic numbers or hard-coded strings that should be constants or configuration values?
  • Could this feature be safely hidden behind a feature flag during rollout?

4. Test Coverage

Automated tests are your safety net. When you review a pull request, check that the tests cover both the happy path and the failure paths. Think about edge cases: what happens with empty input, at boundary values, or under concurrent load?

Consider the type of test as well. Unit tests verify individual functions in isolation. Integration tests verify that components work together. End-to-end tests verify the full user journey. The right mix depends on the change — a new API endpoint warrants integration tests; a utility function warrants a unit test.

If you notice missing test coverage, ask for it. A well-written test also serves as documentation — it shows exactly what the code is expected to do.

5. Performance

Performance issues are often invisible in development environments and only surface in production, where data volumes are larger and traffic is higher. A common example is the N+1 query problem in ORMs like Laravel's Eloquent: a loop that executes one database query per iteration instead of fetching all the needed data in a single query. If you spot this pattern, flag it.

Be aware of the differences between your development and production environments. A query that runs in milliseconds against a handful of rows can take seconds against millions.

6. Knowledge Sharing

Code review is not only about catching problems — it is about learning. Use it as an opportunity to understand parts of the codebase you are less familiar with, and to share knowledge about parts you know well. If you see a pattern that could be reused from elsewhere in the codebase, point it out.

7. Automate the Obvious

Do not spend review time commenting on formatting, whitespace, or bracket placement. A linter handles all of that. Configure it to run on every pull request so that style issues are caught before a human reviewer sees them.

Run your automated tests on pull requests as well. If the tests fail, the reviewer does not need to waste time reading code that is already known to be broken. Consider deploying the PR to a staging environment automatically so that the reviewer can test the change interactively before approving.

8. Be Kind When Commenting

Code review does not have to be a negative experience. If you see something that could be improved, explain why it could be improved and, where possible, suggest an alternative. If you see something done particularly well, say so. Positive and constructive feedback builds trust and creates an environment where people are willing to take risks and learn.

Keep feedback professional. Avoid personal criticism — the goal is to improve the code, not to judge the person who wrote it. Treat every comment as a learning opportunity for both parties.

Research from Google consistently shows that destructive or dismissive feedback in code review damages inclusion and reduces the quality of future contributions. The links below are worth reading:

9. Continuous Improvement Over Perfection

This principle comes from Google's internal code review guidelines. A more experienced developer may look at a less experienced developer's code and find it falls short of their personal standard. That is expected — and it is not a reason to block the pull request. If the code is better than it was before, the review has succeeded. Leave the codebase better than you found it, and trust that the next iteration will be better still.