The topic of code reviews came up in a discussion recently. I was surprised by how strongly even experienced developers defended their positions. Listening more closely, I noticed that many of these convictions reflected not so much a systematic understanding as the habits of their own toolchain. This piqued my curiosity.
In this article, I examine the dimensions that distinguish between different code review approaches – synchronous or asynchronous, blocking or non-blocking – and outline their respective advantages and disadvantages. The aim is to provide teams with guidance on which approach suits their project's criticality, team structure and desired development speed.
Dimensions of code review
Code review processes can be characterised by two independent factors: Timing (synchronous vs. asynchronous) and impact (blocking vs. non-blocking).
| Synchronous | Asynchronous | |
|---|---|---|
| Blocking | Pair / Ensemble Programming | Pull / Merge Requests |
| Non-blocking | Ad-hoc consultations | Post-push |
In a synchronous code review, the reviewer and author are engaged simultaneously, communicating in real time. This approach facilitates immediate feedback, direct communication, and the rapid resolution of questions or concerns. Because all participants are actively present during the review, interactive discussion and clarification happen naturally.
Asynchronous code review allows reviewers and authors to work on their own schedules. The author submits code for review and the reviewers examine it when they are available.
A blocking code review prevents code from being merged until the review is completed and approved. This acts as a gate, ensuring that no change reaches production without explicit validation.
In a non-blocking code review, code can progress even while the review is pending. Feedback is provided afterwards, and corrections are handled subsequently. This approach prioritises development velocity over pre-deployment validation.
Synchronous and blocking
Pair programming and ensemble programming are the clearest examples of synchronous, blocking code review. In these practices, code is written collaboratively with reviewers present from the outset.
In pair programming, two developers work together at a single workstation, simultaneously writing and reviewing code. One developer, the “driver”, writes the code while the other, the “navigator”, reviews it in real time, identifying errors, suggesting improvements, and considering architectural implications. The two developers frequently switch roles.
Ensemble programming, formerly known as “mob programming”, extends this concept to larger teams. Multiple developers collaborate on the same code simultaneously, with one actively coding while the others observe, make suggestions, and consider what comes next.
Pair programming and ensemble programming are particularly valuable in specific scenarios:
Critical changes: When modifying core infrastructure, security-sensitive code, or systems with high business impact, having reviewers present during development ensures that issues are identified and addressed immediately, preventing the introduction of critical bugs.
Knowledge distribution: These practices excel at spreading expertise across teams. Less experienced developers paired with more experienced colleagues can absorb architectural thinking, best practices, and domain knowledge through observation and participation. Complex subsystems benefit from collective understanding.
High-risk refactoring: Large-scale refactoring efforts that affect multiple components benefit from synchronous collaboration to coordinate changes and identify architectural issues early on.
Onboarding: New team members can quickly familiarise themselves with the team's coding standards by working alongside experienced developers.
Synchronous and non-blocking
Ad-hoc consultations are informal, synchronous exchanges in which a developer asks a colleague to look at a piece of code: at a desk, over a video call, or in a chat. Unlike pair programming, these conversations are not planned in advance and do not block the code from being merged. They are particularly useful for quick sanity checks, clarifying design intent, or getting a second opinion on a tricky edge case. They are lightweight and carry little process overhead. But because they are not recorded, they contribute neither to documentation nor to institutional memory.
Asynchronous and blocking
Pull requests, also known as merge requests, have become the dominant code review mechanism for distributed teams. They offer an asynchronous yet blocking approach to code review.
First, a developer creates a feature branch and implements the necessary changes. Then they open a pull request to describe these changes. Team members review the code asynchronously and provide feedback through comments. The code cannot be merged until it has been approved and passed the necessary checks, such as static analysis and automated tests.
Pull requests offer several benefits:
Asynchronous participation: Reviewers can participate at a time that suits them, which is ideal for remote teams and varying time zones.
Documentation: The review discussion becomes part of the project's history, providing context for future developers who encounter the code.
Scalability: The asynchronous nature of the process enables projects to scale without requiring everyone to be available at the same time.
Quality gates: Blocking until approval ensures that no unreviewed code reaches production.
However, pull requests can also introduce friction:
Review latency: Waiting for reviewers can slow down the development process, especially when review capacity is limited or reviewers are in different time zones.
Context switching: Both authors and reviewers must switch context – authors when they receive feedback, and reviewers when they are asked to review.
Reviewer availability: Critical changes may be blocked while waiting for specific reviewers with the necessary expertise.
Long-lived branches: Feature branches can diverge significantly from the main branch, leading to complex merges and potential integration issues.
Asynchronous and non-blocking
Code reviews can also be performed asynchronously and without blocking. This means that the code is reviewed after it has been pushed to the main branch. For this to be viable, substantial engineering discipline, appropriate tooling, and a supporting development process are required.
Trunk-Based Development is one such process: developers make frequent, small commits to a single main branch, known as the “trunk”. Code reviews happen after commits have been pushed, and failures are caught through automated testing and monitoring rather than pre-commit gates.
For this approach to work, any significant feature development must be either hidden behind feature flags or implemented using branch-by-abstraction patterns. This allows incomplete or experimental code to exist in the main branch without affecting end users or other developers.
Feature flags allow code paths to be toggled on or off without redeployment, enabling gradual rollout and immediate rollback if issues arise.
Branch-by-abstraction involves introducing abstraction layers that allow old and new implementations to coexist. The abstraction is then gradually migrated from the old implementation to the new one, after which the old implementation can be removed.
All changes, whether significant or minor, must be easily reversible. This is both a technical and an organisational requirement:
Technical reversibility means having automated rollback capabilities, feature flag toggles, and database migration strategies that support rollback operations.
Organisational reversibility means cultivating a team culture that views rollbacks not as failures but as normal operational safety mechanisms.
Without easy reversibility, the non-blocking nature of Trunk-Based Development becomes dangerous. If a problematic change cannot be quickly reversed, it leads to prolonged incidents that impact the business.
Trunk-Based Development primarily describes the branching strategy in which all developers work frequently and with small commits in a single branch. Whether code review takes place before or after the push is a separate policy question. The core of “non-blocking” lies in optimistic merging: changes are integrated first, and the system trusts that automated tests, monitoring, and downstream reviews will detect problems in a timely and reliable manner.
Conclusion
Code is reviewed to identify defects, share knowledge, enforce standards, and ultimately reduce risk. However, there is no universally optimal approach. Synchronous blocking reviews such as pair programming and ensemble programming maximise immediate feedback and knowledge sharing but can limit development velocity. Asynchronous blocking reviews such as pull or merge requests strike a balance between velocity and safety for many teams. Asynchronous non-blocking reviews maximise velocity but require substantial engineering discipline, comprehensive testing, and reversibility mechanisms.
The most effective teams carefully tailor their code review strategy to their specific context, requirements, and capabilities. Many excellent teams evolve their approach over time, adopting more advanced strategies as their engineering practices mature. The key is understanding the trade-offs and making intentional choices that align with business priorities and team capabilities.
Outlook
Beyond a well-designed review process, team culture is equally important for the effectiveness of code reviews. A constructive feedback culture focuses on behaviour and code rather than on individuals, offers concrete suggestions for improvement, and recognises positive aspects. Rather than getting lost in nitpicking about formatting, reviews should deliberately prioritise architecture, design decisions, and comprehensibility.
Psychological safety plays a central role here: only when everyone feels safe to ask questions, admit uncertainties, and express criticism without fearing negative consequences can code reviews reach their full potential as tools for learning and quality.