How to Design a Code Review & Pull Request Platform
Building a Production-Grade GitHub PR / GitLab MR System — From Git Integration to AI-Powered Reviews
1. Introduction & Why Code Review Platforms Matter
Code review is the single most impactful engineering practice for catching bugs early, sharing knowledge across teams, and maintaining consistent code quality. Platforms like GitHub Pull Requests, GitLab Merge Requests, Bitbucket Pull Requests, and Gerrit Reviews have become the backbone of modern software development workflows. Every day, millions of developers push code, request reviews, discuss changes, and merge branches through these platforms. Designing such a system from scratch requires solving deep problems in distributed systems, version control, real-time collaboration, access control, and user experience.
At its core, a code review platform bridges two worlds: the Git world of commits, branches, and diffs, and the human world of discussions, approvals, and decision-making. The platform must translate raw Git operations into rich, interactive UI experiences — rendering side-by-side diffs of thousands of files, supporting inline discussions pinned to specific lines of code, enforcing complex approval rules, integrating with CI/CD pipelines, and providing real-time notifications to keep everyone in sync. This is far more than a Git wrapper; it is a collaboration platform that orchestrates the social and technical aspects of shipping software.
The market for code review tools is massive. GitHub alone hosts over 100 million repositories and processes billions of pull request events per year. GitLab, Bitbucket, Phabricator, Gerrit, and countless other platforms compete in this space. The barrier to entry is deceptively low — a basic PR system can be built in a weekend — but the gap between a toy implementation and a production-grade platform is enormous. Production systems must handle concurrent edits to the same PR, race conditions between reviews and pushes, complex merge conflict resolution, fine-grained access control, audit logging, and integration with hundreds of external services.
Real-World Case Studies
| Platform | Scale | Unique Feature | Architecture Pattern |
|---|---|---|---|
| GitHub | 100M+ repos, billions of PRs/year | Copilot AI suggestions, Codespaces integration | Microservices on Kubernetes |
| GitLab | 30M+ users | Single application for entire DevOps lifecycle | Monolith decomposing to microservices |
| Bitbucket | 20M+ developers | Deep Jira integration, Pipelines | Atlassian microservices ecosystem |
| Gerrit | Android AOSP, Chromium | Mandatory inline comments, change-based workflow | Java monolith on GWT |
| Phabricator | Meta, Wikimedia | Differential + Diffusion + Herald rules | PHP monolith (archived) |
2. Functional & Non-Functional Requirements
Functional Requirements
The system must support the full pull request lifecycle, from creating a branch and opening a PR to merging and post-merge tracking. Here are the core functional requirements:
- Repository Management: Create, fork, clone, and manage Git repositories. Support public, private, and internal visibility levels.
- Pull Request CRUD: Create, edit, close, reopen, and delete pull requests. Support draft PRs that cannot be merged.
- Diff Rendering: Generate unified, side-by-side, and split diffs. Support file-level, hunk-level, and line-level views. Handle renamed, copied, and binary files.
- Inline Comments: Allow users to comment on specific lines, ranges of lines, or specific files. Support threaded discussions with replies.
- Review Workflow: Submit formal reviews with approve, request changes, or comment verdicts. Support multiple reviews per reviewer.
- Approval & Branch Protection: Enforce configurable approval rules (N reviewers, code owner approval, signed commits, passing CI). Block merges when rules are violated.
- CI/CD Integration: Trigger external CI pipelines on PR events. Display build status, test results, and coverage reports inline.
- Merge Strategies: Support merge commit, squash merge, and rebase merge. Handle fast-forward where possible.
- Conflict Detection: Detect merge conflicts in real-time. Provide resolution guidance and tools.
- CODEOWNERS: Auto-assign reviewers based on file ownership patterns defined in CODEOWNERS files.
- Suggested Changes: Allow reviewers to suggest code changes that authors can apply with one click.
- Notifications: Notify users of PR events via email, in-app, and webhook integrations.
- Search & Discovery: Search PRs by author, reviewer, status, labels, date range, and text content.
- Cherry-Pick: Cherry-pick merged PRs to other branches for backporting fixes.
- Audit Log: Record all actions for compliance and security auditing.
Non-Functional Requirements
| Requirement | Target | Justification |
|---|---|---|
| Availability | 99.95% (4.38 hrs downtime/year) | Developer productivity depends on review platform uptime |
| Latency — page load | < 500ms (p95) | Developers switch between PRs frequently |
| Latency — diff rendering | < 2s for repos up to 10K files | Large diffs must remain usable |
| Latency — notification | < 5s for in-app, < 60s for email | Timely reviews reduce cycle time |
| Throughput | 10K PR creates/sec peak | Large organizations with monorepos |
| Storage | Petabyte-scale Git objects | Years of history across millions of repos |
| Consistency | Strong for approvals, eventual for diffs | Approval state must never be ambiguous |
| Durability | Zero data loss (RPO = 0) | Code and review comments are irreplaceable |
3. High-Level Architecture Overview
The architecture follows a microservices pattern with clear domain boundaries. The PR Service is the central orchestrator that manages the pull request lifecycle. The Diff Service is a specialized compute service that generates diffs by invoking Git operations against object storage. The Review Service handles formal review submissions and approval counting. The Git Service abstracts all raw Git operations (clone, fetch, diff, merge, rebase) and manages Git objects in durable storage. The Event Bus (Kafka) provides async communication between services and ensures that every state change triggers downstream effects (notifications, webhooks, metrics, audit logs).
Service Responsibilities
| Service | Responsibility | Technology | Scaling Strategy |
|---|---|---|---|
| PR Service | PR CRUD, state management, merge orchestration | C# / ASP.NET Core | Horizontal (stateless) |
| Diff Service | Diff generation, patch computation, conflict detection | C# + LibGit2Sharp | Horizontal + job queue |
| Review Service | Review submission, approval counting, verdict tracking | C# / ASP.NET Core | Horizontal (stateless) |
| Git Service | Clone, fetch, push, object storage management | C# + LibGit2Sharp | Worker pool with repo caching |
| Notification Service | Email, in-app, push notifications | C# + SendGrid/Firebase | Horizontal + SQS |
| Search Service | Full-text PR search, filters, saved searches | Elasticsearch | Horizontal (shards) |
| Webhook Dispatcher | Deliver webhook events to external services | C# / Background Service | Horizontal + retry queue |
| Audit Service | Append-only audit log for compliance | C# + Kafka + S3 | Append-only, no scaling needed |
| Metrics Collector | PR metrics, DORA metrics, team analytics | C# + Prometheus + ClickHouse | Horizontal |
4. Git Repository Integration
The Git integration layer is the foundation of the entire platform. Unlike a typical web application that only interacts with a relational database, a code review platform must deeply understand Git internals — commits, trees, blobs, refs, branches, tags, and the packfile format. The platform must handle both read operations (generating diffs, showing file contents, computing blame) and write operations (merging branches, force-pushing, cherry-picking) with correct semantics.
Git Object Model
Git stores data as a content-addressable object store. Every object is identified by its SHA-1 (or SHA-256 in newer Git versions) hash. There are four object types:
- Blob: The content of a file. Identified by the hash of its content. Multiple files with the same content share the same blob.
- Tree: A directory listing mapping file names to blobs or other trees. Represents a snapshot of the repository at a point in time.
- Commit: Points to a tree (the repository snapshot), has parent commit(s), an author, a committer, and a message. This is the fundamental unit of history.
- Tag: A named reference to a commit, optionally signed with GPG for release tagging.
LibGit2Sharp Integration
We use LibGit2Sharp, a managed .NET binding for libgit2, to perform Git operations without shelling out to the git CLI. This gives us fine-grained control over operations, better performance (no process spawn overhead), and the ability to handle edge cases that the CLI abstracts away.
C#
using LibGit2Sharp;
public class GitRepositoryService : IGitRepositoryService
{
private readonly IGitStorageProvider _storage;
private readonly IObjectCache _cache;
public async Task<DiffResult> GenerateDiff(
string repositoryId, string baseSha, string headSha,
DiffOptions options)
{
var repoPath = await _storage.EnsureCloned(repositoryId);
using var repo = new Repository(repoPath);
var baseCommit = repo.Lookup<Commit>(baseSha)
?? throw new NotFoundException($"Base commit {baseSha} not found");
var headCommit = repo.Lookup<Commit>(headSha)
?? throw new NotFoundException($"Head commit {headSha} not found");
var patch = repo.Diff.Compare<Patch>(
baseCommit.Tree,
headCommit.Tree,
new CompareOptions
{
ContextLines = options.ContextLines,
IncludeUnchanged = options.ShowUnchanged,
SimilarityThreshold = options.RenameDetectionThreshold,
TreeComparison = true
});
var fileDiffs = patch.Select(entry => new FileDiff
{
FilePath = entry.Path,
OldPath = entry.OldPath,
Status = MapChangeKind(entry.Status),
hunks = entry.Hunks.Select(h => new DiffHunk
{
OldStart = h.OldStart,
OldLines = h.OldLines,
NewStart = h.NewStart,
NewLines = h.NewLines,
Lines = h.Lines.Select(l => new DiffLine
{
Content = l.Content,
Origin = l.LineOrigin,
OldLineNumber = l.OldLineOffset,
NewLineNumber = l.NewLineOffset
}).ToList()
}).ToList()
}).ToList();
return new DiffResult
{
BaseSha = baseSha,
HeadSha = headSha,
Files = fileDiffs,
Stats = ComputeStats(fileDiffs)
};
}
public async Task<MergeResult> MergeBranch(
string repositoryId, string sourceRef, string targetRef,
MergeStrategy strategy, string userId)
{
var repoPath = await _storage.EnsureCloned(repositoryId);
using var repo = new Repository(repoPath);
var sourceBranch = repo.Branches[sourceRef]
?? throw new NotFoundException($"Branch {sourceRef} not found");
var targetBranch = repo.Branches[targetRef]
?? throw new NotFoundException($"Branch {targetRef} not found");
var mergeResult = repo.Merge(
sourceBranch.Tip,
new Identity(userId, $"{userId}@platform.local"),
new MergeOptions
{
FastForward = strategy == MergeStrategy.FastForward,
FileConflictStrategy = FailOnConflict
});
return new MergeResult
{
Status = mergeResult.Status,
CommitSha = mergeResult.Commit?.Sha,
Conflicts = mergeResult.Conflicts?.Select(c => new ConflictInfo
{
FilePath = c.Ours?.Path ?? c.Theirs?.Path,
Reason = c.Reason.ToString()
}).ToList()
};
}
private static FileChangeStatus MapChangeKind(ChangeKind kind) => kind switch
{
ChangeKind.Added => FileChangeStatus.Added,
ChangeKind.Modified => FileChangeStatus.Modified,
ChangeKind.Deleted => FileChangeStatus.Deleted,
ChangeKind.Renamed => FileChangeStatus.Renamed,
ChangeKind.Copied => FileChangeStatus.Copied,
_ => FileChangeStatus.Modified
};
}
Repository Cloning Strategy
Clone operations are expensive — a large repository can take minutes to clone and consume gigabytes of disk space. We employ a tiered caching strategy:
| Strategy | Description | Use Case |
|---|---|---|
| Full Clone | Complete clone with full history | Active repos (modified in last 7 days) |
| Shallow Clone | Clone with limited depth (--depth=100) | Diff generation (only needs base and head) |
| Partial Clone | Clone without blobs (--filter=blob:none) | Tree browsing, history viewing |
| Minimal Clone | Bare clone with no working tree | Merge and rebase operations |
5. Pull Request Lifecycle
The pull request lifecycle is the core state machine of the platform. Understanding every possible state transition — and the invariants that must hold during each transition — is critical for correctness. Here is the complete lifecycle with all states and transitions:
State Machine Implementation
C#
public enum PullRequestState
{
Draft,
Open,
UnderReview,
ChangesRequested,
Approved,
Merged,
Closed
}
public class PullRequestStateMachine
{
private static readonly Dictionary<(PullRequestState From, PullRequestEvent Event),
PullRequestState> Transitions = new()
{
{(PullRequestState.Draft, PullRequestEvent.ReadyForReview),
PullRequestState.Open},
{(PullRequestState.Draft, PullRequestEvent.Close),
PullRequestState.Closed},
{(PullRequestState.Open, PullRequestEvent.ReviewStarted),
PullRequestState.UnderReview},
{(PullRequestState.Open, PullRequestEvent.ReviewRejected),
PullRequestState.ChangesRequested},
{(PullRequestState.Open, PullRequestEvent.Merge),
PullRequestState.Merged},
{(PullRequestState.UnderReview, PullRequestEvent.AllApproved),
PullRequestState.Approved},
{(PullRequestState.UnderReview, PullRequestEvent.ReviewRejected),
PullRequestState.ChangesRequested},
{(PullRequestState.UnderReview, PullRequestEvent.ReviewCommented),
PullRequestState.Open},
{(PullRequestState.ChangesRequested, PullRequestEvent.NewCommitsPushed),
PullRequestState.Open},
{(PullRequestState.Approved, PullRequestEvent.Merge),
PullRequestState.Merged},
{(PullRequestState.Approved, PullRequestEvent.NewCommitsPushed),
PullRequestState.Open},
{(PullRequestState.Closed, PullRequestEvent.Reopen),
PullRequestState.Open},
};
public PullRequestState Transition(
PullRequestState current, PullRequestEvent evt)
{
if (!Transitions.TryGetValue((current, evt), out var next))
{
throw new InvalidTransitionException(
$"Cannot transition from {current} with event {evt}");
}
return next;
}
}
PR Creation Flow
When a user opens a pull request, the system performs several operations in sequence:
- Validate inputs: Verify source and target branches exist, user has push access to source and read access to target.
- Compute diff: Generate the diff between the merge base of source and target and the head of source. Cache the result.
- Detect conflicts: Attempt a dry-run merge to check for conflicts. If conflicts exist, mark the PR with a warning but still allow creation.
- Assign reviewers: Based on CODEOWNERS rules, round-robin assignment, or manual selection. Auto-add reviewers based on changed files.
- Apply template: If a PR template exists for the target branch, pre-fill the description.
- Persist: Store PR metadata in the database. Emit a
PullRequestCreatedevent to the event bus. - Trigger CI: Send a webhook to the CI system to start pipeline execution on the PR head commit.
Approval Invalidation
A critical correctness requirement: approvals must be invalidated when new commits are pushed to the PR branch. This prevents a scenario where a reviewer approves code, the author pushes additional unreviewed changes, and the PR gets merged with unreviewed code. Our implementation tracks the commit SHA at the time of each approval and rechecks it before merge.
C#
public class ApprovalValidator
{
public async Task<ApprovalStatus> ValidateApprovals(
Guid prId, string targetBranch)
{
var pr = await _prRepository.GetById(prId);
var branchProtection = await _branchProtection.GetRules(targetBranch);
var approvals = await _reviewRepository.GetApprovals(prId);
var validApprovals = approvals
.Where(a => a.CommitSha == pr.HeadSha)
.ToList();
var requiredApprovals = branchProtection.RequiredReviewers;
var requireCodeOwner = branchProtection.RequireCodeOwnerReview;
if (requireCodeOwner)
{
var codeOwners = await _codeOwnership.GetOwners(
pr.ChangedFiles, targetBranch);
var hasCodeOwnerApproval = validApprovals
.Any(a => codeOwners.Contains(a.ReviewerId));
if (!hasCodeOwnerApproval)
return new ApprovalStatus
{
IsApproved = false,
MissingRequirement = "Code owner approval required"
};
}
return new ApprovalStatus
{
IsApproved = validApprovals.Count >= requiredApprovals,
CurrentCount = validApprovals.Count,
RequiredCount = requiredApprovals,
ValidApprovals = validApprovals.Select(a => a.ReviewerId).ToList(),
StaleApprovals = approvals
.Where(a => a.CommitSha != pr.HeadSha).ToList()
};
}
}
6. Data Model & Storage Schema
The data model must capture the rich semantics of pull requests, reviews, comments, and their relationships. Here is the core schema designed for PostgreSQL:
SQL
-- Core pull request table
CREATE TABLE pull_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
repository_id UUID NOT NULL REFERENCES repositories(id),
number SERIAL,
title VARCHAR(500) NOT NULL,
description TEXT,
state VARCHAR(20) NOT NULL DEFAULT 'draft',
author_id UUID NOT NULL REFERENCES users(id),
source_branch VARCHAR(255) NOT NULL,
target_branch VARCHAR(255) NOT NULL,
source_sha VARCHAR(40) NOT NULL,
merge_base_sha VARCHAR(40) NOT NULL,
head_sha VARCHAR(40) NOT NULL,
merge_commit_sha VARCHAR(40),
is_draft BOOLEAN NOT NULL DEFAULT true,
merge_strategy VARCHAR(20) DEFAULT 'merge_commit',
merged_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(repository_id, number)
);
-- Reviews table
CREATE TABLE reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pull_request_id UUID NOT NULL REFERENCES pull_requests(id),
reviewer_id UUID NOT NULL REFERENCES users(id),
verdict VARCHAR(20) NOT NULL, -- approved, changes_requested, commented
body TEXT,
commit_sha VARCHAR(40) NOT NULL,
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_latest BOOLEAN NOT NULL DEFAULT true
);
-- Inline comments / discussions
CREATE TABLE review_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pull_request_id UUID NOT NULL REFERENCES pull_requests(id),
review_id UUID REFERENCES reviews(id),
author_id UUID NOT NULL REFERENCES users(id),
parent_id UUID REFERENCES review_comments(id),
file_path VARCHAR(1000) NOT NULL,
line_number INT,
old_line_number INT,
side VARCHAR(10) NOT NULL, -- RIGHT, LEFT
diff_hunk TEXT NOT NULL,
body TEXT NOT NULL,
resolved BOOLEAN NOT NULL DEFAULT false,
resolved_by UUID REFERENCES users(id),
resolved_at TIMESTAMPTZ,
suggestion TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- General PR comments (not inline)
CREATE TABLE pull_request_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pull_request_id UUID NOT NULL REFERENCES pull_requests(id),
author_id UUID NOT NULL REFERENCES users(id),
parent_id UUID REFERENCES pull_request_comments(id),
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Status checks / CI results
CREATE TABLE status_checks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pull_request_id UUID NOT NULL REFERENCES pull_requests(id),
context VARCHAR(255) NOT NULL,
state VARCHAR(20) NOT NULL, -- pending, success, failure, error
target_url TEXT,
description VARCHAR(500),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
output JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- CODEOWNERS
CREATE TABLE code_owners (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
repository_id UUID NOT NULL REFERENCES repositories(id),
pattern VARCHAR(500) NOT NULL,
owner_ids UUID[] NOT NULL,
branch VARCHAR(255) DEFAULT 'main',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Branch protection rules
CREATE TABLE branch_protection_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
repository_id UUID NOT NULL REFERENCES repositories(id),
branch_pattern VARCHAR(255) NOT NULL,
require_pull_request BOOLEAN DEFAULT true,
required_reviewers INT DEFAULT 1,
require_code_owner_review BOOLEAN DEFAULT false,
require_status_checks BOOLEAN DEFAULT false,
required_status_checks TEXT[],
require_signed_commits BOOLEAN DEFAULT false,
require_linear_history BOOLEAN DEFAULT false,
allow_force_pushes BOOLEAN DEFAULT false,
allow_deletions BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Audit log (append-only)
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
actor_id UUID NOT NULL,
action VARCHAR(100) NOT NULL,
resource VARCHAR(100) NOT NULL,
resource_id UUID,
metadata JSONB,
ip_address INET,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_log_actor ON audit_log(actor_id);
CREATE INDEX idx_audit_log_action ON audit_log(action);
CREATE INDEX idx_audit_log_created ON audit_log(created_at);
CREATE INDEX idx_pr_state ON pull_requests(repository_id, state);
CREATE INDEX idx_pr_author ON pull_requests(author_id, created_at DESC);
CREATE INDEX idx_reviews_pr ON reviews(pull_request_id, is_latest);
CREATE INDEX idx_comments_pr ON review_comments(pull_request_id, file_path);
reviews table, the is_latest flag is a denormalized optimization. Instead of using a subquery to find the latest review per reviewer, we maintain a boolean that is set to true only for the most recent review by each reviewer on each PR. When a new review is submitted, the previous is_latest review for that reviewer is set to false. This turns a complex window-function query into a simple indexed lookup.
7. Diff Rendering Engine
The diff rendering engine is one of the most compute-intensive and user-visible components of the platform. It must take raw Git diff output and transform it into a rich, interactive, line-numbered view that developers can use to leave inline comments. The engine supports three viewing modes: unified (single column, showing changes inline), side-by-side (two columns, old on left and new on right), and split (same as side-by-side but with synchronized scrolling).
Diff Algorithm
We use the Myers diff algorithm (the same algorithm used by Git itself) with patience diff as a fallback for files where Myers produces poor results (such as files with many moved blocks). The algorithm runs in O((N+M)D) time where N and M are the lengths of the two inputs and D is the edit script length. For files with very large diffs, we cap the output at a configurable maximum (default 3000 lines) and indicate truncation to the user.
C#
public class DiffRenderer
{
private readonly DiffAlgorithm _algorithm;
private readonly HunkSplitter _hunkSplitter;
public RenderedDiff Render(DiffInput input, DiffViewMode mode)
{
var rawDiff = _algorithm.ComputeDiff(
input.OldContent, input.NewContent);
var hunks = _hunkSplitter.SplitIntoHunks(rawDiff,
contextLines: 3);
var renderedHunks = hunks.Select(hunk =>
{
if (mode == DiffViewMode.Unified)
return RenderUnified(hunk);
else
return RenderSideBySide(hunk);
}).ToList();
return new RenderedDiff
{
FilePath = input.FilePath,
FileMode = input.FileMode,
hunks = renderedHunks,
Stats = ComputeLineStats(rawDiff),
IsTruncated = rawDiff.TotalLines > MaxDisplayLines
};
}
private RenderedHunk RenderSideBySide(DiffHunk hunk)
{
var leftLines = new List<SideBySideLine>();
var rightLines = new List<SideBySideLine>();
foreach (var line in hunk.Lines)
{
switch (line.Origin)
{
case LineOrigin.Context:
leftLines.Add(new SideBySideLine
{
LineNumber = line.OldLineNumber,
Content = line.Content,
Type = LineType.Context
});
rightLines.Add(new SideBySideLine
{
LineNumber = line.NewLineNumber,
Content = line.Content,
Type = LineType.Context
});
break;
case LineOrigin.Deletion:
leftLines.Add(new SideBySideLine
{
LineNumber = line.OldLineNumber,
Content = line.Content,
Type = LineType.Deletion
});
rightLines.Add(new SideBySideLine
{
LineNumber = null,
Content = "",
Type = LineType.Empty
});
break;
case LineOrigin.Addition:
leftLines.Add(new SideBySideLine
{
LineNumber = null,
Content = "",
Type = LineType.Empty
});
rightLines.Add(new SideBySideLine
{
LineNumber = line.NewLineNumber,
Content = line.Content,
Type = LineType.Addition
});
break;
case LineOrigin.HunkHeader:
leftLines.Add(new SideBySideLine
{
Content = line.Content,
Type = LineType.HunkHeader
});
rightLines.Add(new SideBySideLine
{
Content = line.Content,
Type = LineType.HunkHeader
});
break;
}
}
return new RenderedHunk
{
LeftLines = leftLines,
RightLines = rightLines,
OldStart = hunk.OldStart,
NewStart = hunk.NewStart
};
}
private RenderedHunk RenderUnified(DiffHunk hunk)
{
var lines = hunk.Lines.Select(l => new UnifiedLine
{
LineNumber = l.Origin == LineOrigin.Deletion
? l.OldLineNumber : l.NewLineNumber,
Content = l.Content,
Type = l.Origin switch
{
LineOrigin.Context => LineType.Context,
LineOrigin.Addition => LineType.Addition,
LineOrigin.Deletion => LineType.Deletion,
LineOrigin.HunkHeader => LineType.HunkHeader,
_ => LineType.Context
}
}).ToList();
return new RenderedHunk { Lines = lines };
}
}
Diff Caching Strategy
Diff computation is expensive for large files. We cache rendered diffs in Redis with the following key structure:
- Key:
diff:{repoId}:{baseSha}:{headSha}:{viewMode} - TTL: 24 hours (diffs are immutable once computed)
- Invalidation: Never (content-addressable — same inputs always produce same output)
- Storage: Compressed JSON (typically 10-50KB per diff for files under 1000 lines)
8. Inline Code Comments & Discussions
Inline comments are the heart of code review. They allow reviewers to attach discussions to specific lines of code, creating contextual feedback that is far more valuable than general comments. The design must support:
- Line-level pinning: Comments are pinned to a specific line in the diff, identified by file path + line number + side (old/new).
- Range selection: Reviewers can select a range of lines and comment on the entire selection.
- Threaded replies: Comments support parent-child relationships, forming discussion threads.
- Outdated detection: When new commits are pushed, comments on changed lines are marked as "outdated" if the surrounding code has changed.
- Resolution: Thread authors and PR authors can resolve threads, hiding resolved discussions.
- Suggestions: Comment bodies can contain code suggestions that can be applied with one click.
Comment Positioning Model
Pinning comments to line numbers is fragile — line numbers shift as commits are pushed. Instead, we use a three-part position key:
C#
public class CommentPosition
{
public string FilePath { get; set; }
public int? OldLineStart { get; set; } // line in base (left side)
public int? NewLineStart { get; set; } // line in head (right side)
public string DiffHunkHeader { get; set; } // "@@ -10,7 +10,8 @@" for context
public string CommitSha { get; set; } // snapshot at time of comment
/// <summary>
/// Translates comment position to current diff if the underlying
/// lines have shifted due to new commits.
/// </summary>
public TranslatedPosition TranslateTo(string currentHeadSha)
{
if (CommitSha == currentHeadSha)
return new TranslatedPosition(this);
var currentDiff = _diffService.GetDiff(CommitSha, currentHeadSha, FilePath);
var hunks = currentDiff.Hunks;
// Find the hunk that contains this line and track its drift
foreach (var hunk in hunks)
{
if (OldLineStart.HasValue &&
OldLineStart >= hunk.OldStart &&
OldLineStart < hunk.OldStart + hunk.OldLines)
{
var drift = hunk.NewStart - hunk.OldStart;
return new TranslatedPosition
{
FilePath = FilePath,
LineNumber = OldLineStart + drift,
Side = CommentSide.Left,
IsOutdated = false
};
}
}
return new TranslatedPosition
{
FilePath = FilePath,
LineNumber = OldLineStart,
Side = CommentSide.Left,
IsOutdated = true
};
}
}
Real-Time Updates
When a user leaves a comment, all other users viewing the same PR should see it in real-time without refreshing. We use SignalR (WebSocket) connections for this:
C#
public class PrHub : Hub
{
private readonly ICommentRepository _comments;
public async Task JoinPullRequest(Guid prId)
{
await Groups.AddToGroupAsync(
Context.ConnectionId, $"pr:{prId}");
}
public async Task LeavePullRequest(Guid prId)
{
await Groups.RemoveFromGroupAsync(
Context.ConnectionId, $"pr:{prId}");
}
public async Task NewComment(PullRequestCommentDto dto)
{
var comment = await _comments.Create(dto);
await Clients.Group($"pr:{dto.PullRequestId}")
.SendAsync("CommentReceived", comment);
// Also emit to event bus for notifications and webhooks
await _eventBus.Publish(new CommentCreatedEvent
{
CommentId = comment.Id,
PullRequestId = dto.PullRequestId,
AuthorId = dto.AuthorId
});
}
}
9. Review Assignment Strategies
Getting the right reviewers assigned to a PR is critical for fast, high-quality reviews. Poor assignment leads to stale PRs waiting days for review, or worse, reviews from people unfamiliar with the changed code. The platform supports multiple assignment strategies:
CODEOWNERS-Based Assignment
When a PR modifies files, the system reads the CODEOWNERS file to determine which users or teams own those files. The CODEOWNERS file uses gitignore-style patterns:
# CODEOWNERS file (placed in .github/CODEOWNERS)
# Default owners for everything
* @platform-team
# Backend services
src/backend/ @backend-team @alice
# Frontend
src/frontend/ @frontend-team @bob @charlie
# Infrastructure
infra/ @devops-team
*.tf @devops-team
# Database migrations
migrations/ @dba-team @alice
# API documentation
docs/api/ @docs-team
# Security-critical paths
src/auth/ @security-team (requires 2 approvals)
src/crypto/ @security-team (requires 2 approvals)
Round-Robin Assignment
Within a code owner team, reviewers are assigned using weighted round-robin to balance workload:
C#
public class RoundRobinAssigner : IReviewerAssigner
{
private readonly IReviewHistoryRepository _history;
public async Task<List<ReviewerAssignment>> Assign(
List<string> candidatePool, Guid repositoryId, int count)
{
// Get recent review counts per candidate in last 30 days
var recentCounts = await _history
.GetReviewCounts(candidatePool, repositoryId,
DateTimeOffset.UtcNow.AddDays(-30));
// Weight inversely by review count (less reviews = higher weight)
var weights = candidatePool.ToDictionary(
c => c,
c => 1.0 / (1.0 + recentCounts.GetValueOrDefault(c, 0)));
var assigned = new List<ReviewerAssignment>();
for (int i = 0; i < count; i++)
{
var totalWeight = weights
.Where(w => !assigned.Any(a => a.ReviewerId == w.Key))
.Sum(w => w.Value);
var random = new Random();
var roll = random.NextDouble() * totalWeight;
var cumulative = 0.0;
foreach (var (candidate, weight) in weights)
{
if (assigned.Any(a => a.ReviewerId == candidate))
continue;
cumulative += weight;
if (roll <= cumulative)
{
assigned.Add(new ReviewerAssignment
{
ReviewerId = candidate,
Strategy = AssignmentStrategy.RoundRobin,
Reason = $"Balanced assignment (score: {weight:F2})"
});
break;
}
}
}
return assigned;
}
}
Assignment Strategies Comparison
| Strategy | Best For | Weakness |
|---|---|---|
| CODEOWNERS | Large teams, regulatory compliance | Must maintain CODEOWNERS file |
| Round-Robin | Balancing workload within teams | Ignores expertise matching |
| Manual | Critical/sensitive PRs | Doesn't scale, creates bottlenecks |
| Blame-based | Assigning people who last touched the code | May not be the right expert today |
| AI-suggested | Complex repos with unclear ownership | Requires training data, may be inaccurate |
10. Approval Rules & Branch Protection
Branch protection rules are the enforcement mechanism that ensures code quality standards are met before changes can be merged. They are configured per-branch and can be as simple as "require 1 approval" or as complex as "require 2 approvals from security team, passing status checks from 3 CI pipelines, signed commits, and linear history only."
Branch Protection Rule Engine
C#
public class BranchProtectionEvaluator
{
public async Task<MergeGateResult> Evaluate(
Guid prId, string targetBranch)
{
var rules = await _protectionRepo.GetRules(targetBranch);
if (rules == null || !rules.RequirePullRequest)
return MergeGateResult.Allowed();
var pr = await _prRepo.GetById(prId);
var results = new List<RuleCheck>();
// Check 1: Required reviewers
if (rules.RequiredReviewers > 0)
{
var approvalStatus = await _approvalValidator
.ValidateApprovals(prId, targetBranch);
results.Add(new RuleCheck
{
Name = "Required Reviewers",
Passed = approvalStatus.IsApproved,
Details = $"{approvalStatus.CurrentCount}/{approvalStatus.RequiredCount} approvals"
});
}
// Check 2: Code owner review
if (rules.RequireCodeOwnerReview)
{
var owners = await _codeOwnership.GetOwners(
pr.ChangedFiles, targetBranch);
var approvals = await _reviewRepo.GetApprovals(prId);
var hasOwnerApproval = approvals
.Any(a => owners.Contains(a.ReviewerId) &&
a.CommitSha == pr.HeadSha);
results.Add(new RuleCheck
{
Name = "Code Owner Review",
Passed = hasOwnerApproval,
Details = hasOwnerApproval ? "Approved by code owner" : "Missing"
});
}
// Check 3: Required status checks
if (rules.RequireStatusChecks)
{
var checks = await _statusCheckRepo
.GetLatestChecks(prId, rules.RequiredStatusChecks);
var allPassed = checks.All(c => c.State == "success");
var failedChecks = checks
.Where(c => c.State != "success")
.Select(c => c.Context)
.ToList();
results.Add(new RuleCheck
{
Name = "Status Checks",
Passed = allPassed,
Details = allPassed ? "All checks passed"
: $"Failed: {string.Join(", ", failedChecks)}"
});
}
// Check 4: Signed commits
if (rules.RequireSignedCommits)
{
var isSigned = await _gitService
.VerifyCommitsSigned(pr.SourceBranch);
results.Add(new RuleCheck
{
Name = "Signed Commits",
Passed = isSigned,
Details = isSigned ? "All commits signed" : "Unsigned commits found"
});
}
// Check 5: Linear history (no merge commits)
if (rules.RequireLinearHistory)
{
var isLinear = await _gitService
.IsLinearHistory(pr.SourceBranch, pr.TargetBranch);
results.Add(new RuleCheck
{
Name = "Linear History",
Passed = isLinear,
Details = isLinear ? "History is linear"
: "Merge commits detected"
});
}
return new MergeGateResult
{
CanMerge = results.All(r => r.Passed),
Checks = results,
EvaluatedAt = DateTimeOffset.UtcNow
};
}
}
11. CI/CD Integration & Status Checks
Continuous integration is inseparable from code review. The platform must trigger CI pipelines when a PR is opened or updated, display build status in the PR UI, and block merges when required status checks fail. The integration model supports both push-based (webhooks from CI system) and pull-based (platform polls CI system) patterns.
Status Check Flow
Status Check Payload
C#
public class StatusCheckPayload
{
public string State { get; set; } // pending, success, failure, error
public string TargetUrl { get; set; } // link to CI build logs
public string Description { get; set; } // "3 tests passed", "Build failed"
public string Context { get; set; } // "ci/junit", "lint/eslint", "security/snyk"
// For detailed status (GitHub-style combined status)
public List<CheckRun> CheckRuns { get; set; }
// For rich status (GitLab-style pipelines)
public PipelineInfo Pipeline { get; set; }
}
public class CheckRun
{
public string Name { get; set; }
public string Status { get; set; } // queued, in_progress, completed
public string Conclusion { get; set; } // success, failure, skipped, cancelled
public AnnotationsSummary Output { get; set; }
public List<Annotation> Annotations { get; set; }
}
public class Annotation
{
public string Path { get; set; }
public int Line { get; set; }
public string Level { get; set; } // notice, warning, failure
public string Message { get; set; }
public string Title { get; set; }
}
Annotations from CI systems are mapped to inline comments in the PR diff view. When a linter reports "line 42: unused variable 'x'", the platform displays this as an annotation badge on line 42 of the relevant file. This tight integration between CI and the review UI is what makes modern code review platforms so powerful.
12. Merge Strategies
Merge strategies determine how the source branch commits are integrated into the target branch. Each strategy has different implications for history cleanliness, commit attribution, and bisectability. The platform must support all three major strategies and enforce the configured strategy via branch protection.
Strategy Comparison
| Strategy | History Shape | Preserves Individual Commits | Clean History | Best For |
|---|---|---|---|---|
| Merge Commit | Merge bubble | Yes | No (bubbles accumulate) | Feature branches with meaningful commit sequences |
| Squash | Linear | No (all squashed to one commit) | Yes | Noisy branches with WIP commits |
| Rebase | Linear | Yes | Yes | Clean linear history with individual commits |
Merge Implementation
C#
public class MergeExecutor
{
private readonly IGitRepositoryService _git;
private readonly IAuditLogger _audit;
public async Task<MergeResult> ExecuteMerge(
PullRequest pr, MergeStrategy strategy, string userId)
{
return strategy switch
{
MergeStrategy.MergeCommit =>
await ExecuteMergeCommit(pr, userId),
MergeStrategy.Squash =>
await ExecuteSquash(pr, userId),
MergeStrategy.Rebase =>
await ExecuteRebase(pr, userId),
_ => throw new ArgumentException($"Unknown strategy: {strategy}")
};
}
private async Task<MergeResult> ExecuteSquash(
PullRequest pr, string userId)
{
var squashMessage = FormatSquashMessage(pr);
var result = await _git.SquashMerge(
pr.RepositoryId,
sourceRef: $"refs/heads/{pr.SourceBranch}",
targetRef: $"refs/heads/{pr.TargetBranch}",
message: squashMessage,
author: userId);
await _audit.Log(new AuditEntry
{
ActorId = userId,
Action = "pull_request.merged_squash",
Resource = "pull_request",
ResourceId = pr.Id,
Metadata = new
{
pr.Number,
pr.SourceBranch,
pr.TargetBranch,
CommitCount = pr.CommitCount,
ResultSha = result.CommitSha
}
});
return result;
}
private string FormatSquashMessage(PullRequest pr)
{
var body = pr.Description?.Trim() ?? "";
if (!string.IsNullOrEmpty(body))
return $"{pr.Title}\n\n{body}\n\nSquashed from PR #{pr.Number}";
return $"{pr.Title}\n\nSquashed from PR #{pr.Number}";
}
private async Task<MergeResult> ExecuteRebase(
PullRequest pr, string userId)
{
// Ensure linear history (no merge commits in source branch)
var hasMergeCommits = await _git.HasMergeCommits(
pr.RepositoryId, pr.SourceBranch, pr.TargetBranch);
if (hasMergeCommits)
throw new MergeConflictException(
"Cannot rebase: source branch contains merge commits. " +
"Use merge commit strategy or enable linear history enforcement.");
return await _git.RebaseOnto(
pr.RepositoryId,
sourceRef: $"refs/heads/{pr.SourceBranch}",
targetRef: $"refs/heads/{pr.TargetBranch}",
author: userId);
}
}
13. Conflict Detection & Resolution
Merge conflicts occur when the same lines of the same file are modified in both the source and target branches since they diverged. Detecting and resolving conflicts is one of the most frustrating aspects of the developer experience, and the platform must make it as smooth as possible.
Proactive Conflict Detection
We detect conflicts proactively at multiple points:
- On PR creation: Check for conflicts and warn the author.
- On target branch push: When any branch protected by a PR is updated, recheck all open PRs targeting that branch for new conflicts.
- Periodic sweep: A background job runs every 5 minutes to detect conflicts in PRs that haven't been recently checked.
- On merge attempt: Final conflict check before executing the merge.
C#
public class ConflictDetector
{
private readonly IGitRepositoryService _git;
public async Task<ConflictReport> DetectConflicts(Guid prId)
{
var pr = await _prRepo.GetById(prId);
var mergeResult = await _git.AttemptMerge(
pr.RepositoryId,
sourceRef: pr.SourceBranch,
targetRef: pr.TargetBranch,
dryRun: true);
if (mergeResult.Status != MergeStatus.Conflicts)
{
return new ConflictReport
{
HasConflicts = false,
ConflictedFiles = new List<ConflictDetail>()
};
}
var details = mergeResult.Conflicts.Select(c =>
{
var sourceContent = await _git.GetFileContent(
pr.RepositoryId, pr.HeadSha, c.FilePath);
var targetContent = await _git.GetFileContent(
pr.RepositoryId, pr.TargetBranch, c.FilePath);
return new ConflictDetail
{
FilePath = c.FilePath,
Reason = c.Reason,
ConflictMarkers = ParseConflictMarkers(
sourceContent, targetContent),
ResolutionGuidance = GenerateResolutionGuidance(c)
};
}).ToList();
return new ConflictReport
{
HasConflicts = true,
ConflictedFiles = details,
SuggestedResolutions = details.Count > 0
? new List<string> { "Rebase source branch onto target" }
: new List<string>()
};
}
private List<ConflictMarker> ParseConflictMarkers(
string source, string target)
{
// Parse git conflict markers:
// <<<<<<< HEAD
// target content
// =======
// source content
// >>>>>>> branch-name
var markers = new List<ConflictMarker>();
var lines = source.Split('\n');
ConflictMarker current = null;
foreach (var line in lines)
{
if (line.StartsWith("<<<<<<<"))
{
current = new ConflictMarker { Type = ConflictType.Incoming };
}
else if (line.StartsWith("======="))
{
current.Type = ConflictType.Base;
}
else if (line.StartsWith(">>>>>>>"))
{
markers.Add(current);
current = null;
}
else if (current != null)
{
current.Content.AppendLine(line);
}
}
return markers;
}
}
Conflict Resolution UI
The platform provides an in-browser conflict resolution editor. For each conflicted file, the user sees three panes: the base version (before changes), their version (incoming), and the target version (current). They can select which changes to keep for each conflict marker and edit the result directly in the browser. The resolved content is then committed as a new commit to the source branch.
14. CODEOWNERS & Code Ownership
The CODEOWNERS file extends the review assignment system by defining file-level ownership. The system must parse CODEOWNERS files (supporting multiple locations: root, .github/, docs/), match file paths against patterns, and resolve ownership hierarchies.
CODEOWNERS Parsing Logic
C#
public class CodeOwnersParser
{
public List<OwnershipRule> Parse(string content)
{
var rules = new List<OwnershipRule>();
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.StartsWith('#') || string.IsNullOrEmpty(trimmed))
continue;
// Parse: pattern owner1 owner2
var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) continue;
var pattern = parts[0];
var owners = parts.Skip(1)
.Select(o => o.TrimStart('@'))
.ToList();
rules.Add(new OwnershipRule
{
Pattern = pattern,
Owners = owners,
RequiresApprovals = ParseApprovalCount(parts)
});
}
// Later rules override earlier ones (last match wins)
return rules;
}
public List<string> GetOwners(
List<string> changedFiles, List<OwnershipRule> rules)
{
var owners = new HashSet<string>();
foreach (var file in changedFiles)
{
// Match rules in reverse order (last match wins)
var matchingRules = rules
.Where(r => MatchesPattern(file, r.Pattern))
.ToList();
if (matchingRules.Any())
{
var lastMatch = matchingRules.Last();
foreach (var owner in lastMatch.Owners)
owners.Add(owner);
}
}
return owners.ToList();
}
private bool MatchesPattern(string filePath, string pattern)
{
// Support gitignore-style patterns:
// *: matches any file
// **: matches zero or more directories
// *.cs: matches all C# files
// src/: matches everything under src/
if (pattern.Contains("**"))
{
var regex = "^" + Regex.Escape(pattern)
.Replace("\\*\\*\\/", "(.*/)?")
.Replace("\\*", "[^/]*") + "$";
return Regex.IsMatch(filePath, regex);
}
if (pattern.Contains('*'))
{
var regex = "^" + Regex.Escape(pattern)
.Replace("\\*", ".*") + "$";
return Regex.IsMatch(filePath, regex);
}
if (pattern.EndsWith('/'))
return filePath.StartsWith(pattern);
return filePath == pattern;
}
}
15. Suggested Changes & Auto-Apply
Suggested changes are a powerful feature that allows reviewers to propose exact code modifications within their comments. Authors can apply these suggestions with a single click, which creates a new commit with the suggestion applied. This eliminates the back-and-forth of "can you change X to Y" comments.
Suggestion Format
Suggestions use a fenced code block within the comment body with a special suggestion language tag:
// Reviewer's comment body:
// "This variable should be `async` to match the call pattern."
//
// ```suggestion
// var result = await ProcessAsync(request);
// ```
//
// The ```suggestion block contains the replacement text for the
// selected lines.
Apply Suggestion Implementation
C#
public class SuggestionApplier
{
private readonly IGitRepositoryService _git;
private readonly IReviewCommentRepository _comments;
public async Task<ApplyResult> ApplySuggestion(
Guid commentId, string userId)
{
var comment = await _comments.GetById(commentId);
if (string.IsNullOrEmpty(comment.Suggestion))
throw new InvalidOperationException("Comment has no suggestion");
var pr = await _prRepo.GetById(comment.PullRequestId);
// Create a temporary branch, apply the change, commit
var tempBranch = $"suggestion/{pr.Number}/{commentId}";
await _git.CreateBranch(
pr.RepositoryId, tempBranch, pr.HeadSha);
// Apply the suggestion to the file
var fileContent = await _git.GetFileContent(
pr.RepositoryId, pr.HeadSha, comment.FilePath);
var updatedContent = ApplySuggestionToFile(
fileContent, comment, comment.Suggestion);
await _git.UpdateFile(
pr.RepositoryId, tempBranch,
comment.FilePath, updatedContent,
commitMessage: $"Apply suggestion from @{comment.AuthorUsername}",
authorId: userId);
// Create a commit with the co-author
var commitSha = await _git.Commit(
pr.RepositoryId, tempBranch,
$"Apply suggestion from @{comment.AuthorUsername}\n\n" +
$"Co-authored-by: {comment.AuthorUsername} " +
$"<{comment.AuthorUsername}@users.noreply.github.com>",
authorId: userId);
// Force-update the PR branch
await _git.ForceUpdateRef(
pr.RepositoryId, $"refs/heads/{pr.SourceBranch}", commitSha);
// Mark the comment thread as resolved
await _comments.ResolveThread(commentId, userId);
return new ApplyResult
{
CommitSha = commitSha,
FilePath = comment.FilePath,
Resolved = true
};
}
}
16. Automated Code Review
Beyond human reviews, the platform integrates automated code review tools that run on every PR. These include linters (ESLint, Prettier, RuboCop), static analyzers (SonarQube, CodeClimate), security scanners (Snyk, Dependabot), and increasingly, AI-powered review tools (GitHub Copilot, CodeRabbit, Codium).
Automated Review Pipeline
AI Code Review Integration
AI-powered code review is the fastest-growing segment of automated review. The platform exposes a webhook-based integration model where external AI services can subscribe to PR events, analyze diffs, and post review comments. The internal flow:
C#
public class AiReviewOrchestrator
{
private readonly IWebhookDispatcher _webhooks;
private readonly IPullRequestRepository _prs;
private readonly IDiffService _diff;
public async Task TriggerAiReview(Guid prId)
{
var pr = await _prs.GetById(prId);
var diff = await _diff.GetDiff(
pr.RepositoryId, pr.MergeBaseSha, pr.HeadSha);
// Filter to only source code files (skip configs, assets, etc.)
var reviewableFiles = diff.Files
.Where(f => IsReviewableLanguage(f.FilePath))
.ToList();
if (!reviewableFiles.Any()) return;
// Dispatch to registered AI review services
await _webhooks.Dispatch(new AiReviewRequestedEvent
{
PullRequestId = prId,
RepositoryId = pr.RepositoryId,
HeadSha = pr.HeadSha,
Files = reviewableFiles.Select(f => new AiFileInput
{
Path = f.FilePath,
Patch = f.Patch,
Language = DetectLanguage(f.FilePath)
}).ToList(),
ReviewContext = new AiReviewContext
{
RepoLanguage = pr.Repository.PrimaryLanguage,
CodingStandards = await _GetCodingStandards(pr.RepositoryId),
PreviousReviews = await _GetRecentReviewPatterns(prId)
}
});
}
public async Task ProcessAiReviewComment(
Guid prId, AiReviewComment comment)
{
// Validate the AI service's identity
var service = await _webhookService
.GetService(comment.ServiceId);
if (!service.IsAiReviewEnabled)
throw new UnauthorizedAccessException();
// Create inline comments for each AI suggestion
foreach (var suggestion in comment.Suggestions)
{
await _comments.Create(new ReviewCommentCreateDto
{
PullRequestId = prId,
AuthorId = service.BotUserId,
FilePath = suggestion.FilePath,
LineNumber = suggestion.LineNumber,
Body = $"🤖 **AI Review:** {suggestion.Message}\n\n" +
$"```suggestion\n{suggestion.SuggestedCode}\n```",
IsAutomated = true
});
}
// Post summary review
await _reviews.Submit(new ReviewCreateDto
{
PullRequestId = prId,
ReviewerId = service.BotUserId,
Verdict = ReviewVerdict.Commented,
Body = $"Automated review by {service.Name}: " +
$"{comment.Suggestions.Count} suggestions found."
});
}
}
17. API Design
The API follows RESTful conventions with resource-oriented URLs. All endpoints require authentication via Bearer tokens (JWT) and enforce authorization based on repository visibility and user permissions.
Pull Request Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/repos/{repoId}/prs | List PRs with filters (state, author, reviewer, labels) |
POST | /api/v1/repos/{repoId}/prs | Create a new pull request |
GET | /api/v1/repos/{repoId}/prs/{number} | Get PR details |
PATCH | /api/v1/repos/{repoId}/prs/{number} | Update PR title, description, reviewers |
POST | /api/v1/repos/{repoId}/prs/{number}/merge | Merge the PR |
POST | /api/v1/repos/{repoId}/prs/{number}/close | Close the PR |
POST | /api/v1/repos/{repoId}/prs/{number}/reopen | Reopen the PR |
POST | /api/v1/repos/{repoId}/prs/{number}/draft | Convert to/from draft |
GET | /api/v1/repos/{repoId}/prs/{number}/diff | Get the full diff |
GET | /api/v1/repos/{repoId}/prs/{number}/files | List changed files with stats |
POST | /api/v1/repos/{repoId}/prs/{number}/reviews | Submit a review (approve/request changes/comment) |
GET | /api/v1/repos/{repoId}/prs/{number}/comments | List all comments on the PR |
POST | /api/v1/repos/{repoId}/prs/{number}/comments | Post a comment |
POST | /api/v1/repos/{repoId}/prs/{number}/cherry-pick | Cherry-pick to another branch |
Example: Create Pull Request
HTTP
POST /api/v1/repos/abc123/prs
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
{
"title": "Add user authentication middleware",
"description": "## Changes\n- Added JWT auth middleware\n- Added token refresh endpoint\n- Updated API routes to require auth\n\n## Testing\n- [x] Unit tests passing\n- [ ] Integration tests pending",
"source_branch": "feature/auth-middleware",
"target_branch": "main",
"reviewers": ["alice", "bob"],
"labels": ["backend", "security"],
"is_draft": false,
"auto_merge": {
"enabled": true,
"strategy": "squash",
"when": "when_checks_pass"
}
}
HTTP
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/repos/abc123/prs/42
{
"id": "pr-uuid-42",
"number": 42,
"state": "open",
"title": "Add user authentication middleware",
"url": "https://platform.example.com/repos/abc123/pulls/42",
"diff_url": "https://platform.example.com/repos/abc123/pulls/42.diff",
"created_at": "2026-01-15T10:30:00Z",
"mergeable": true,
"merge_conflicts": false
}
18. Draft Pull Requests
Draft PRs are a critical workflow feature that allows developers to share work-in-progress without signaling that the code is ready for review. Draft PRs cannot be merged, cannot be approved, and do not trigger review assignment. They serve as a signal: "I'm working on this, you can look at it, but don't review it yet."
Draft PR Semantics
| Feature | Draft PR | Ready PR |
|---|---|---|
| Merge button | Disabled | Enabled (subject to rules) |
| Approval requests | Not sent | Auto-assigned per CODEOWNERS |
| Reviews | Can comment, cannot approve | Full review capabilities |
| CI status | Shown (but not blocking) | Shown and blocking if required |
| Notifications | Muted (daily digest only) | Real-time |
| Auto-merge | Disabled | Can be enabled |
The transition from draft to ready-for-review is a deliberate action by the author. This triggers review assignment, notification dispatch, and CI status evaluation. The system must handle the edge case where a user pushes commits to a draft PR that triggers CI failures — these failures should be visible but not block the eventual transition to ready-for-review.
19. Cherry-Pick Workflows
Cherry-picking is the process of applying specific commits from one branch to another. It is essential for hotfix workflows where a critical bug fix must be backported to older release branches. The platform provides a UI-driven cherry-pick flow that creates a new PR with the cherry-picked commits.
Cherry-Pick Flow
C#
public class CherryPickService
{
private readonly IGitRepositoryService _git;
public async Task<CherryPickResult> CherryPick(
Guid prId, string targetBranch, string userId)
{
var pr = await _prRepo.GetById(prId);
if (pr.State != PullRequestState.Merged)
throw new InvalidOperationException("Can only cherry-pick merged PRs");
var commits = await _git.GetCommits(
pr.RepositoryId, pr.MergeBaseSha, pr.HeadSha);
var cherryPickBranch =
$"cherry-pick/{targetBranch}/pr-{pr.Number}";
await _git.CreateBranch(
pr.RepositoryId, cherryPickBranch, targetBranch);
var cherryPickedSha = pr.MergeCommitSha;
foreach (var commit in commits.Reverse())
{
try
{
cherryPickedSha = await _git.CherryPick(
pr.RepositoryId,
commit.Sha,
cherryPickBranch,
userId);
}
catch (CherryPickConflictException ex)
{
return new CherryPickResult
{
Success = false,
ConflictFiles = ex.ConflictedFiles,
Message = $"Conflict cherry-picking {commit.Sha}: " +
$"{string.Join(", ", ex.ConflictedFiles)}"
};
}
}
// Create a new PR for the cherry-pick
var newPr = await _prService.Create(new CreatePrDto
{
RepositoryId = pr.RepositoryId,
Title = $"[Cherry-pick #{pr.Number}] {pr.Title}",
Description = $"Cherry-pick of PR #{pr.Number} to {targetBranch}\n\n" +
$"Original PR: #{pr.Number}\n" +
$"Original author: @{pr.AuthorUsername}",
SourceBranch = cherryPickBranch,
TargetBranch = targetBranch,
Reviewers = pr.Reviewers.Select(r => r.UserId).ToList()
});
return new CherryPickResult
{
Success = true,
NewPrNumber = newPr.Number,
CherryPickedCommits = commits.Count
};
}
}
20. Monorepo Support
Monorepos — repositories containing multiple services, libraries, or applications — present unique challenges for code review. A single PR might touch files across 5 different services, but only 2 of those services need to be reviewed. The platform must support path-based PR scoping, selective CI triggers, and granular review assignment.
Monorepo Challenges
- Large diffs: A monorepo with 100K+ files can produce massive diffs. The diff service must support file-level pagination and lazy loading.
- Selective CI: Only the affected packages/services should be built and tested, not the entire monorepo.
- CODEOWNERS complexity: Code ownership rules must be hierarchical and support deep path patterns.
- Merge order: When a PR affects multiple interdependent packages, the merge order matters. The platform must detect dependency relationships and enforce merge ordering.
- PR splitting: Users should be able to split a large PR into smaller, focused PRs based on affected packages.
Path-Based Review Routing
| Changed Paths | Assigned Team | Required Approvals | CI Triggered |
|---|---|---|---|
services/auth/** | @auth-team | 2 | auth-service CI |
services/billing/** | @billing-team | 2 | billing-service CI |
libs/shared/** | @platform-team | 3 | All downstream CIs |
infra/** | @devops-team | 2 | infra-tests CI |
21. Large File & LFS Handling
Git Large File Storage (LFS) replaces large files with pointers in Git while storing the actual content on a separate server. The platform must integrate with LFS to display large file changes correctly, handle LFS lock management, and ensure that diffs for LFS-tracked files show the appropriate metadata (file size change, lock status) rather than attempting to diff binary content.
LFS Integration Points
- Diff rendering: For LFS-tracked files, display file size before/after instead of binary diff. For text-like LFS files (JSON, XML), optionally render content diff.
- Lock management: LFS locks prevent concurrent edits to large binary files. The platform displays lock status in the PR and warns when a file is locked by another user.
- Storage management: LFS objects are stored in object storage (S3/GCS) with a CDN for fast access. The platform pre-warms LFS objects when a PR is opened.
- Pruning: Periodic LFS cleanup removes orphaned objects. Orphaned LFS objects are those whose parent commits are garbage-collected.
| File Type | Size | Diff Strategy | Storage |
|---|---|---|---|
| Source code (<1MB) | Small | Full text diff | Git objects |
| Images (<10MB) | Medium | Side-by-side preview | Git LFS |
| Binaries (>10MB) | Large | Size change only | Git LFS |
| Datasets (>100MB) | Very large | Hash comparison | Git LFS + CDN |
22. Notification System
The notification system must keep developers informed about PR events without overwhelming them. Too many notifications lead to alert fatigue; too few lead to stale PRs that nobody reviews. The system supports multiple channels and delivery strategies:
Notification Channels
| Channel | Use Case | Latency | Batching |
|---|---|---|---|
| In-app (SignalR) | Real-time UI updates | < 1s | None |
| Review requests, merge notifications | < 60s | Daily digest option | |
| Push (FCM/APNs) | Mobile app notifications | < 10s | None |
| Webhook | External integrations (Slack, Teams, Jira) | < 5s | Configurable |
| SMS | Critical hotfix PRs only | < 30s | Never batched |
Notification Event Matrix
| Event | Author | Reviewers | Subscribers | Mentioned |
|---|---|---|---|---|
| PR opened | — | All assigned | — | All |
| Review requested | — | Requested | — | — |
| Review submitted | Yes | — | — | — |
| Comment posted | Yes | All participants | — | Mentioned only |
| CI status changed | Yes | — | — | — |
| Merged | Yes | All reviewers | — | — |
| Conflict detected | Yes | — | — | — |
| Approvals stale | Yes | Stale approvers | — | — |
23. Review Metrics & Analytics
Metrics and analytics help engineering leaders understand and improve their code review process. The platform tracks key DORA metrics and review-specific metrics:
Key Metrics
| Metric | Definition | Target | Calculation |
|---|---|---|---|
| Time to First Review | Time from PR creation to first review | < 4 hours | min(review.submitted_at) - pr.created_at |
| Time to Merge | Time from PR creation to merge | < 1 business day | pr.merged_at - pr.created_at |
| Review Depth | Average comments per PR | 3-8 | count(comments) / count(prs) |
| Approval Rate | % of PRs approved on first submission | > 60% | approved_first_try / total_prs |
| PR Size | Lines changed per PR | < 400 lines | sum(lines_added + lines_removed) |
| Review Turnaround | Time from review request to review submission | < 8 hours | review.submitted_at - review_requested_at |
| Stale PR Rate | % of PRs with no activity for > 7 days | < 5% | stale_prs / total_open_prs |
| Churn Rate | % of PRs requiring > 1 round of review | < 30% | prs_with_changes_requested / total_prs |
Metrics Dashboard
The metrics dashboard uses ClickHouse for fast analytical queries on large datasets. Key views include:
SQL
-- Average time to merge by team (last 30 days)
SELECT
t.name as team_name,
AVG(EXTRACT(EPOCH FROM (pr.merged_at - pr.created_at)) / 3600)
as avg_hours_to_merge,
PERCENTILE_CONT(0.5) WITHIN GROUP
(ORDER BY EXTRACT(EPOCH FROM (pr.merged_at - pr.created_at)) / 3600)
as median_hours_to_merge,
COUNT(*) as total_prs_merged
FROM pull_requests pr
JOIN teams t ON pr.author_team_id = t.id
WHERE pr.merged_at > NOW() - INTERVAL '30 days'
AND pr.state = 'merged'
GROUP BY t.name
ORDER BY avg_hours_to_merge;
-- PR size distribution (last 30 days)
SELECT
CASE
WHEN lines_changed <= 50 THEN 'XS (1-50)'
WHEN lines_changed <= 200 THEN 'S (51-200)'
WHEN lines_changed <= 500 THEN 'M (201-500)'
WHEN lines_changed <= 1000 THEN 'L (501-1000)'
ELSE 'XL (1000+)'
END as size_bucket,
COUNT(*) as pr_count,
AVG(EXTRACT(EPOCH FROM (merged_at - created_at)) / 3600)
as avg_hours_to_merge
FROM pull_requests
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY size_bucket
ORDER BY MIN(lines_changed);
-- Reviewer workload balance
SELECT
u.username,
COUNT(r.id) as reviews_last_30d,
AVG(EXTRACT(EPOCH FROM
(r.submitted_at - review_requested_at)) / 3600)
as avg_response_hours
FROM reviews r
JOIN users u ON r.reviewer_id = u.id
WHERE r.submitted_at > NOW() - INTERVAL '30 days'
GROUP BY u.username
ORDER BY reviews_last_30d DESC;
25. Webhooks & Integrations
Webhooks allow external services to receive real-time notifications about PR events. They are the primary integration mechanism for connecting the code review platform with project management tools (Jira, Linear), chat platforms (Slack, Teams), deployment systems (ArgoCD, Flux), and custom internal tools.
Webhook Event Types
| Event | Payload | Trigger |
|---|---|---|
pull_request.opened | Full PR object with diff stats | PR created |
pull_request.synchronize | PR object with new head SHA | New commits pushed |
pull_request.review_submitted | Review object with verdict | Review submitted |
pull_request.comment | Comment object with file/line context | Comment posted |
pull_request.merged | PR object with merge commit SHA | PR merged |
pull_request.closed | PR object | PR closed without merging |
status_check.completed | Check result with annotations | CI pipeline completes |
Webhook Delivery with Retry
C#
public class WebhookDispatcher
{
private readonly HttpClient _http;
private readonly IWebhookRepository _repo;
private readonly IMessageQueue _queue;
public async Task Dispatch<T>(string eventType, T payload)
{
var subscriptions = await _repo
.GetSubscriptions(eventType, payload.RepositoryId);
foreach (var sub in subscriptions)
{
var event = new WebhookEvent
{
EventType = eventType,
Payload = JsonSerializer.Serialize(payload),
DeliveryId = Guid.NewGuid(),
SubscriptionId = sub.Id,
AttemptCount = 0
};
await _queue.Enqueue(event);
}
}
public async Task ProcessDelivery(WebhookEvent evt)
{
var sub = await _repo.GetSubscription(evt.SubscriptionId);
var signature = ComputeHmacSha256(
evt.Payload, sub.Secret);
try
{
var response = await _http.SendAsync(new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(sub.Url),
Headers =
{
{"X-Webhook-Event", evt.EventType},
{"X-Webhook-Delivery", evt.DeliveryId.ToString()},
{"X-Webhook-Signature", $"sha256={signature}"}
},
Content = new StringContent(
evt.Payload,
Encoding.UTF8,
"application/json")
});
if (!response.IsSuccessStatusCode &&
evt.AttemptCount < 5)
{
// Exponential backoff: 1m, 5m, 30m, 2h, 8h
var delays = new[] {1, 5, 30, 120, 480};
var delay = TimeSpan.FromMinutes(
delays[evt.AttemptCount]);
await _queue.EnqueueWithDelay(evt, delay);
}
}
catch (HttpRequestException)
{
if (evt.AttemptCount < 5)
await _queue.EnqueueWithDelay(evt,
TimeSpan.FromMinutes(1));
}
}
}
26. Security, Compliance & Audit
Code review platforms handle the most sensitive assets in an organization — source code, secrets, intellectual property, and access credentials. Security is not optional; it is foundational.
Security Measures
- Authentication: OAuth 2.0 / OIDC with support for GitHub, GitLab, Google, and SAML SSO. MFA required for all accounts.
- Authorization: Role-based access control (RBAC) with repository-level permissions: read, triage, write, maintain, admin.
- Secret scanning: Automatic detection of API keys, passwords, and tokens in PR diffs. Block merge when secrets are detected.
- Branch protection: Server-side enforcement of all rules. Never trust client-side validation.
- Audit logging: Append-only audit log of all actions, stored in immutable storage (WORM). Retained for 7 years for SOC 2 compliance.
- Encryption: TLS 1.3 for data in transit, AES-256 for data at rest, encrypted backups.
- IP allowlisting: Optional IP-based access restrictions for enterprise customers.
- Dependency scanning: Integration with Dependabot/Snyk to alert on vulnerable dependencies in PR diffs.
Audit Log Schema
JSON
{
"id": "audit-uuid-123",
"timestamp": "2026-01-15T14:30:00Z",
"actor": {
"id": "user-uuid",
"username": "alice",
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0..."
},
"action": "pull_request.merged",
"resource": {
"type": "pull_request",
"id": "pr-uuid-42",
"repository": "org/backend-service",
"details": {
"pr_number": 42,
"source_branch": "feature/auth",
"target_branch": "main",
"merge_strategy": "squash",
"merge_commit_sha": "abc123def456"
}
},
"authorization": {
"permission_used": "write",
"branch_protection_evaluated": true,
"checks_passed": ["2-approvals", "ci-passing", "signed-commits"]
}
}
27. Monitoring & Observability
The platform requires comprehensive monitoring across three pillars: metrics, logs, and traces. Key metrics include:
| Metric | Type | Alert Threshold | Dashboard |
|---|---|---|---|
| PR creation rate | Counter | Anomaly detection | Operational Overview |
| Merge rate | Counter | < 50% of creation rate | Flow Dashboard |
| Diff generation latency | Histogram | p99 > 5s | Performance Dashboard |
| Git clone duration | Histogram | p95 > 120s | Git Service Health |
| Webhook delivery rate | Counter | Error rate > 5% | Integration Health |
| Notification latency | Histogram | p99 > 60s | Notification Dashboard |
| Review wait time | Gauge | Avg > 24 hours | Team Velocity |
| Search query latency | Histogram | p95 > 2s | Search Performance |
| Disk usage (Git clones) | Gauge | > 80% capacity | Infrastructure |
| Database connection pool | Gauge | > 80% utilized | Database Health |
28. Cost Estimation
Building and operating a code review platform at scale involves significant infrastructure costs. Here is a detailed cost breakdown for a platform serving 1 million active repositories and 100,000 active developers:
| Component | Specification | Monthly Cost | Notes |
|---|---|---|---|
| Web/API Servers | 16x c6i.2xlarge (8 vCPU, 16GB) | $5,500 | Stateless, auto-scaling |
| Diff Workers | 8x c6i.4xlarge (16 vCPU, 32GB) | $4,400 | Burst-capable, queue-driven |
| Git Service Workers | 16x m6i.2xlarge (8 vCPU, 32GB) | $5,600 | Memory-heavy for Git operations |
| PostgreSQL | db.r6g.2xlarge Multi-AZ (2 instances) | $3,200 | Primary + read replica |
| Redis | cache.r6g.xlarge cluster (3 nodes) | $1,500 | Session, diff cache, queues |
| Elasticsearch | 3x m6i.xlarge.search (32GB) | $1,800 | PR search index |
| ClickHouse | 4x i3.2xlarge (8 vCPU, 64GB) | $2,200 | Metrics analytics |
| Kafka | 3x kafka.m5.2xlarge | $2,000 | Event streaming |
| S3 (Git objects) | 50TB storage + transfer | $1,200 | Git blobs + LFS objects |
| CDN | 5TB monthly transfer | $400 | Static assets + LFS fetch |
| SignalR (WebSocket) | 2x Standard instances | $200 | Real-time updates |
| Email (SES/SendGrid) | 2M emails/month | $500 | Review requests + digests |
| Monitoring (Prometheus/Grafana) | Self-hosted on EC2 | $800 | Metrics + alerting |
| Total | $29,300/mo | ~$351,600/year |
29. Testing Strategy
Testing a code review platform requires a multi-layered approach. The most critical tests are those that verify the correctness of the state machine, diff rendering, merge operations, and access control:
Test Layers
| Layer | Scope | Tools | Coverage Target |
|---|---|---|---|
| Unit Tests | Individual service logic (state machine, diff parsing, approval counting) | xUnit, FluentAssertions, Moq | 90%+ |
| Integration Tests | Service-to-database, service-to-Git operations | Testcontainers (PostgreSQL, Redis), LibGit2Sharp in-memory repos | 80%+ |
| Contract Tests | API contract validation between services | Pact | 100% API surface |
| E2E Tests | Full PR lifecycle workflows | Playwright (browser), REST client | Critical paths |
| Performance Tests | Diff generation, merge operations, concurrent PRs | k6, NBomber | SLA compliance |
Critical Test Scenarios
C#
public class PrStateMachineTests
{
[Fact]
public async Task Cannot_Approve_Draft_PR()
{
var pr = CreateDraftPr();
var stateMachine = new PullRequestStateMachine();
var exception = await Assert.ThrowsAsync<InvalidTransitionException>(
() => stateMachine.TransitionAsync(pr,
PullRequestEvent.Approve));
Assert.Contains("Draft", exception.Message);
}
[Fact]
public async Task Approval_Is_Invalidated_On_New_Commit()
{
var pr = CreateOpenPr(headSha: "commit-a");
await _reviewService.SubmitReview(pr.Id,
reviewer: "alice",
verdict: ReviewVerdict.Approved,
commitSha: "commit-a");
// Author pushes new commit
await _prService.UpdateHeadSha(pr.Id, "commit-b");
// Approval should be stale
var status = await _approvalValidator.ValidateApprovals(
pr.Id, "main");
Assert.False(status.IsApproved);
Assert.Single(status.StaleApprovals);
}
[Fact]
public async Task Branch_Protection_Blocks_Merge_When_Checks_Fail()
{
var pr = CreateApprovedPr();
await _statusCheckService.UpdateStatus(
pr.Id, "ci/junit", "failure");
var gate = await _branchProtection.Evaluate(
pr.Id, "main");
Assert.False(gate.CanMerge);
Assert.Contains(gate.Checks,
c => c.Name == "Status Checks" && !c.Passed);
}
[Fact]
public async Task Squash_Merge_Combines_All_Commits()
{
var pr = CreatePrWithCommits(5);
var result = await _mergeExecutor.ExecuteMerge(
pr, MergeStrategy.Squash, userId: "alice");
var commits = await _git.GetCommits(
pr.RepositoryId,
pr.MergeBaseSha,
result.CommitSha);
// Only the squash commit should exist after merge base
Assert.Single(commits);
}
}
30. Interview Q&A Deep Dive
Q1: How would you handle a PR with 10,000+ changed files?
A PR with 10,000+ changed files is likely a mass refactoring, rename, or auto-generated change. The diff service must handle this efficiently by: (1) paginating the file list and loading diffs lazily, (2) using background computation to pre-render all file diffs and caching them in Redis, (3) collapsing trivial files (renames-only, whitespace-only) into a "X files changed" summary, and (4) allowing users to filter by path pattern, file type, or change type. The UI must never attempt to render all 10,000 file diffs simultaneously — it loads diffs on-demand as the user scrolls.
Q2: How do you prevent race conditions when two reviewers approve simultaneously?
Approval counting must be atomic. We use PostgreSQL's SELECT ... FOR UPDATE on the PR row when updating approval counts. Alternatively, we use an append-only model: reviews are inserted (never updated), and approval count is computed as COUNT(*) WHERE verdict='approved' AND commit_sha = current_head_sha. This eliminates the need for locking because the count is always derived from the immutable reviews table. The merge gate check uses a serializable transaction to ensure that two concurrent approval checks don't both conclude "approved" when only one approval was needed.
Q3: How would you design the diff algorithm to handle very large files (100K+ lines)?
For very large files, we use a streaming diff algorithm that processes the file in chunks. The Myers algorithm is run in streaming mode: the two file contents are read from Git objects in chunks of 10,000 lines, and the diff is computed per-chunk with overlap to handle hunk boundaries. The rendered diff is paginated — the API returns hunk-level data, and the client loads hunks on demand. We also cap the total rendered diff at 3,000 lines per file, showing a "truncated" indicator beyond that.
Q4: How do you handle force-pushes to a PR branch?
Force-pushes are dangerous because they rewrite history. When a force-push is detected (the parent of the new head commit doesn't include the previous head), we: (1) invalidate all existing reviews (since the commits they reviewed no longer exist), (2) mark all inline comments as "potentially outdated" and re-run the position translation algorithm, (3) reset the merge gate to "not approved", and (4) send a notification to all reviewers that the PR was force-pushed and their reviews were reset. The force-push itself is allowed (the platform doesn't prevent it), but all accumulated trust is reset.
Q5: How do you ensure that secrets are never committed in PRs?
We run secret scanning as a pre-merge check. The scanner uses regex patterns for known secret formats (AWS keys, API tokens, private keys) combined with entropy analysis for unknown patterns. The scanner runs on every PR push (as a status check) and uses a streaming approach to process files without loading entire large files into memory. When a potential secret is detected, it's flagged as a "warning" annotation on the specific line. Branch protection rules can require the secret scan to pass before merge. For additional security, we partner with services like GitLeaks and TruffleHog for comprehensive pattern matching.
Q6: How would you design the notification system to avoid email fatigue?
We implement a multi-tier notification system: (1) Real-time in-app notifications via SignalR for immediate awareness, (2) Email notifications with intelligent batching — the first event in a 10-minute window triggers an immediate email, subsequent events are batched into a digest, (3) User-configurable notification preferences per event type and per repository, (4) A "watching" model where users can choose to watch a PR at different levels: "participating" (only when mentioned), "all activity" (every comment and review), or "release only" (merge/close events only), (5) Daily and weekly digest emails summarizing PR activity across repositories the user is interested in.
Q7: How do you handle merge conflicts for large monorepo PRs?
For monorepo PRs with many conflicted files, we provide: (1) Per-file conflict resolution rather than requiring the developer to resolve all conflicts at once, (2) An interactive 3-way merge editor in the browser that shows base/ours/theirs with one-click resolution for each conflict marker, (3) A "rebase onto target" button that automates the entire rebase process locally (using a background worker) and pushes the result, (4) Dependency-aware conflict resolution that detects when a conflict in a shared library affects downstream packages and suggests resolving the library first, (5) Conflict detection runs on every target branch push and proactively notifies PR authors of new conflicts before they attempt to merge.
Q8: Explain the trade-offs between storing Git objects in S3 vs. local disk.
S3 (object storage): Virtually unlimited scale, 99.999999999% durability, pay-per-use, but higher latency per access (50-200ms per request). Best for durable storage of all Git objects. Local disk: Very low latency (<1ms), but limited by instance storage (typically 100GB-1TB per instance), requires RAID for durability, and doesn't scale. Best for caching frequently-accessed repos. Our architecture uses a tiered approach: S3 is the source of truth for all Git objects. Local disk is used as a read-through cache for active repos. When a Git operation needs an object not on local disk, it's fetched from S3, stored locally, and served. The LRU eviction policy keeps disk usage bounded. This gives us S3's durability and scale with local-disk latency for active repos.
Q9: How do you implement the "suggested changes" feature?
The "suggested changes" feature uses a special markdown fence syntax (```suggestion) in review comments. The system: (1) Parses the suggestion block and the comment's file path + line numbers to determine exactly which lines to replace, (2) When the author clicks "Apply suggestion," creates a temporary branch from the current PR head, replaces the specified lines in the file, commits the change with the author as committer and the reviewer as co-author, force-updates the PR branch reference to the new commit, and resolves the comment thread. This is all done server-side to ensure atomicity. The suggestion commit is a regular Git commit, so it appears in the PR's commit history and can be reverted independently.
Q10: How do you handle concurrent edits to the same PR description or title?
PR metadata (title, description) uses optimistic concurrency control. Each update includes the updated_at timestamp of the last known version. If the timestamp doesn't match (another user updated concurrently), the server returns a 409 Conflict with the current version. The client then shows both versions and allows the user to merge their changes. For inline comments, we use append-only semantics — comments are never edited in place (instead, edits create a new revision stored in the comment_edits table). This avoids all concurrency issues on comment content.