system-design50 min read

How to Design a Code Review & Pull Request Platform — A Senior+ Guide | Ayodhyya

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

Senior+ System Design Guide 10,000+ Words 25 Deep-Dive Sections C# · Mermaid · Real-World Case Studies

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.

Key Insight: A code review platform is fundamentally a state machine built on top of Git. The pull request is the central entity that transitions through states (draft → open → review → approved → merged → closed), and every action — pushing a commit, leaving a comment, approving, requesting changes — triggers a state transition. The complexity lies in making these transitions atomic, consistent, and real-time across thousands of concurrent users.

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

PlatformScaleUnique FeatureArchitecture Pattern
GitHub100M+ repos, billions of PRs/yearCopilot AI suggestions, Codespaces integrationMicroservices on Kubernetes
GitLab30M+ usersSingle application for entire DevOps lifecycleMonolith decomposing to microservices
Bitbucket20M+ developersDeep Jira integration, PipelinesAtlassian microservices ecosystem
GerritAndroid AOSP, ChromiumMandatory inline comments, change-based workflowJava monolith on GWT
PhabricatorMeta, WikimediaDifferential + Diffusion + Herald rulesPHP 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

RequirementTargetJustification
Availability99.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 filesLarge diffs must remain usable
Latency — notification< 5s for in-app, < 60s for emailTimely reviews reduce cycle time
Throughput10K PR creates/sec peakLarge organizations with monorepos
StoragePetabyte-scale Git objectsYears of history across millions of repos
ConsistencyStrong for approvals, eventual for diffsApproval state must never be ambiguous
DurabilityZero data loss (RPO = 0)Code and review comments are irreplaceable

3. High-Level Architecture Overview

graph TB Client[Web/Mobile Client] --> LB[Load Balancer] LB --> APIGateway[API Gateway] APIGateway --> PRService[PR Service] APIGateway --> DiffService[Diff Service] APIGateway --> ReviewService[Review Service] APIGateway --> NotificationService[Notification Service] APIGateway --> SearchService[Search Service] PRService --> GitService[Git Service] PRService --> DB[(PostgreSQL Cluster)] DiffService --> GitService DiffService --> DiffCache[(Redis Cache)] ReviewService --> DB ReviewService --> EventBus[Event Bus - Kafka] GitService --> GitStorage[(Git Object Storage - S3)] EventBus --> NotificationService EventBus --> WebhookService[Webhook Dispatcher] EventBus --> AuditService[Audit Logger] EventBus --> MetricsCollector[Metrics Collector] PRService --> CIConnector[CI/CD Connector] SearchService --> ElasticSearch[(Elasticsearch)]

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).

Design Decision: We separate the Git Service from the PR Service because Git operations are fundamentally different from relational database operations. Git is content-addressable, immutable, and requires compute-intensive operations (diff, merge, rebase). By isolating Git operations behind a service boundary, we can scale Git compute independently (using ephemeral workers that clone repos on demand) without bottlenecking the PR Service's database operations.

Service Responsibilities

ServiceResponsibilityTechnologyScaling Strategy
PR ServicePR CRUD, state management, merge orchestrationC# / ASP.NET CoreHorizontal (stateless)
Diff ServiceDiff generation, patch computation, conflict detectionC# + LibGit2SharpHorizontal + job queue
Review ServiceReview submission, approval counting, verdict trackingC# / ASP.NET CoreHorizontal (stateless)
Git ServiceClone, fetch, push, object storage managementC# + LibGit2SharpWorker pool with repo caching
Notification ServiceEmail, in-app, push notificationsC# + SendGrid/FirebaseHorizontal + SQS
Search ServiceFull-text PR search, filters, saved searchesElasticsearchHorizontal (shards)
Webhook DispatcherDeliver webhook events to external servicesC# / Background ServiceHorizontal + retry queue
Audit ServiceAppend-only audit log for complianceC# + Kafka + S3Append-only, no scaling needed
Metrics CollectorPR metrics, DORA metrics, team analyticsC# + Prometheus + ClickHouseHorizontal

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.
graph LR C1[Commit abc123] --> T1[Tree root] T1 --> B1[Blob: src/Program.cs] T1 --> T2[Tree: src/] T1 --> T3[Tree: tests/] T2 --> B2[Blob: src/Service.cs] T3 --> B3[Blob: tests/ServiceTest.cs] C2[Commit def456] --> C1 C2 --> T4[Tree root v2]

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:

StrategyDescriptionUse Case
Full CloneComplete clone with full historyActive repos (modified in last 7 days)
Shallow CloneClone with limited depth (--depth=100)Diff generation (only needs base and head)
Partial CloneClone without blobs (--filter=blob:none)Tree browsing, history viewing
Minimal CloneBare clone with no working treeMerge and rebase operations
Operational Concern: At scale, managing thousands of Git clones on disk requires careful storage management. We use a least-recently-used (LRU) eviction policy: repos not accessed in 30 days are deleted from the local cache and re-cloned on demand from the durable object storage (S3). This keeps disk usage bounded while ensuring fast access for active repositories.

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:

stateDiagram-v2 [*] --> Draft : Create PR Draft --> Open : Ready for Review Draft --> Closed : Abandon Open --> UnderReview : Reviewer starts review Open --> ChangesRequested : Review rejects UnderReview --> Approved : All reviewers approve UnderReview --> ChangesRequested : Reviewer rejects UnderReview --> Open : Reviewer comments (no verdict) ChangesRequested --> Open : Author pushes fixes Approved --> Merged : Merge button clicked Approved --> Open : New commits pushed (revokes approval) Open --> Merged : Auto-merge (CI passes) Merged --> [*] Closed --> Open : Reopen Closed --> [*]

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:

  1. Validate inputs: Verify source and target branches exist, user has push access to source and read access to target.
  2. Compute diff: Generate the diff between the merge base of source and target and the head of source. Cache the result.
  3. Detect conflicts: Attempt a dry-run merge to check for conflicts. If conflicts exist, mark the PR with a warning but still allow creation.
  4. Assign reviewers: Based on CODEOWNERS rules, round-robin assignment, or manual selection. Auto-add reviewers based on changed files.
  5. Apply template: If a PR template exists for the target branch, pre-fill the description.
  6. Persist: Store PR metadata in the database. Emit a PullRequestCreated event to the event bus.
  7. 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);
Performance Tip: For the 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)
Edge Case: Binary files (images, PDFs, compiled binaries) require special handling. We render binary file diffs as a "Binary file not shown" placeholder with file size comparison. For image files specifically, we show a side-by-side preview of the old and new versions using the Git blob content. For large binary files exceeding 10MB, we skip rendering entirely and show only metadata (file size change).

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

StrategyBest ForWeakness
CODEOWNERSLarge teams, regulatory complianceMust maintain CODEOWNERS file
Round-RobinBalancing workload within teamsIgnores expertise matching
ManualCritical/sensitive PRsDoesn't scale, creates bottlenecks
Blame-basedAssigning people who last touched the codeMay not be the right expert today
AI-suggestedComplex repos with unclear ownershipRequires 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
        };
    }
}
Security Warning: Branch protection rules must be enforced server-side, never client-side. A malicious user could bypass client-side checks by using the API directly. All merge operations must be validated against branch protection rules in the server-side merge handler, and the merge gate evaluation must happen within a database transaction to prevent race conditions.

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

sequenceDiagram participant Dev as Developer participant PR as PR Service participant Git as Git Service participant CI as CI/CD System participant DB as Database Dev->>PR: Push commits to PR branch PR->>Git: Get head SHA PR->>CI: Webhook (PR updated, head_sha) CI->>CI: Start pipeline CI-->>PR: Status: pending (webhook) PR->>DB: Store status check (pending) PR->>PR: Broadcast via SignalR Note over Dev: UI shows pending indicator CI->>CI: Run tests, linting, build CI-->>PR: Status: success/failure (webhook) PR->>DB: Update status check PR->>PR: Re-evaluate merge gate PR->>PR: Broadcast via SignalR Note over Dev: UI shows green check / red X

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

StrategyHistory ShapePreserves Individual CommitsClean HistoryBest For
Merge CommitMerge bubbleYesNo (bubbles accumulate)Feature branches with meaningful commit sequences
SquashLinearNo (all squashed to one commit)YesNoisy branches with WIP commits
RebaseLinearYesYesClean 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);
    }
}
Best Practice: Squash merge is the most popular strategy for teams that want clean linear history. It collapses all WIP commits ("fix typo", "address review", "oops wrong file") into a single clean commit on the target branch. The trade-off is that individual commit history is lost on the target branch — but the PR itself preserves the full history for reference.

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:

  1. On PR creation: Check for conflicts and warn the author.
  2. On target branch push: When any branch protected by a PR is updated, recheck all open PRs targeting that branch for new conflicts.
  3. Periodic sweep: A background job runs every 5 minutes to detect conflicts in PRs that haven't been recently checked.
  4. 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

graph LR A[PR Opened/Updated] --> B[Linting] A --> C[Static Analysis] A --> D[Security Scan] A --> E[AI Code Review] B --> F[Results Aggregator] C --> F D --> F E --> F F --> G[Post Review Comments] F --> H[Update Status Checks] F --> I[Notify Author]

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

MethodEndpointDescription
GET/api/v1/repos/{repoId}/prsList PRs with filters (state, author, reviewer, labels)
POST/api/v1/repos/{repoId}/prsCreate 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}/mergeMerge the PR
POST/api/v1/repos/{repoId}/prs/{number}/closeClose the PR
POST/api/v1/repos/{repoId}/prs/{number}/reopenReopen the PR
POST/api/v1/repos/{repoId}/prs/{number}/draftConvert to/from draft
GET/api/v1/repos/{repoId}/prs/{number}/diffGet the full diff
GET/api/v1/repos/{repoId}/prs/{number}/filesList changed files with stats
POST/api/v1/repos/{repoId}/prs/{number}/reviewsSubmit a review (approve/request changes/comment)
GET/api/v1/repos/{repoId}/prs/{number}/commentsList all comments on the PR
POST/api/v1/repos/{repoId}/prs/{number}/commentsPost a comment
POST/api/v1/repos/{repoId}/prs/{number}/cherry-pickCherry-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

FeatureDraft PRReady PR
Merge buttonDisabledEnabled (subject to rules)
Approval requestsNot sentAuto-assigned per CODEOWNERS
ReviewsCan comment, cannot approveFull review capabilities
CI statusShown (but not blocking)Shown and blocking if required
NotificationsMuted (daily digest only)Real-time
Auto-mergeDisabledCan 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

sequenceDiagram participant Dev as Developer participant PR as PR Service participant Git as Git Service Dev->>PR: Request cherry-pick of PR #42 to release/2.1 PR->>Git: Get merged commits from PR #42 PR->>Git: Create branch cherry-pick/release-2.1/pr-42 loop For each commit in PR #42 PR->>Git: Cherry-pick commit onto release/2.1 end PR->>PR: Create new PR (cherry-pick/release-2.1/pr-42 → release/2.1) PR->>PR: Auto-assign reviewers from original PR PR->>Dev: Cherry-pick PR created (#43)
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 PathsAssigned TeamRequired ApprovalsCI Triggered
services/auth/**@auth-team2auth-service CI
services/billing/**@billing-team2billing-service CI
libs/shared/**@platform-team3All downstream CIs
infra/**@devops-team2infra-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 TypeSizeDiff StrategyStorage
Source code (<1MB)SmallFull text diffGit objects
Images (<10MB)MediumSide-by-side previewGit LFS
Binaries (>10MB)LargeSize change onlyGit LFS
Datasets (>100MB)Very largeHash comparisonGit 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

ChannelUse CaseLatencyBatching
In-app (SignalR)Real-time UI updates< 1sNone
EmailReview requests, merge notifications< 60sDaily digest option
Push (FCM/APNs)Mobile app notifications< 10sNone
WebhookExternal integrations (Slack, Teams, Jira)< 5sConfigurable
SMSCritical hotfix PRs only< 30sNever batched

Notification Event Matrix

EventAuthorReviewersSubscribersMentioned
PR openedAll assignedAll
Review requestedRequested
Review submittedYes
Comment postedYesAll participantsMentioned only
CI status changedYes
MergedYesAll reviewers
Conflict detectedYes
Approvals staleYesStale approvers
Smart Batching: The notification system implements intelligent batching for email notifications. Instead of sending one email per comment, it batches all comments within a 10-minute window into a single digest. The first event in a window triggers an immediate email; subsequent events within the window are accumulated and sent as a single "3 new updates on PR #42" email. This reduces email volume by ~80% while ensuring timely delivery of the first notification.

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

MetricDefinitionTargetCalculation
Time to First ReviewTime from PR creation to first review< 4 hoursmin(review.submitted_at) - pr.created_at
Time to MergeTime from PR creation to merge< 1 business daypr.merged_at - pr.created_at
Review DepthAverage comments per PR3-8count(comments) / count(prs)
Approval Rate% of PRs approved on first submission> 60%approved_first_try / total_prs
PR SizeLines changed per PR< 400 linessum(lines_added + lines_removed)
Review TurnaroundTime from review request to review submission< 8 hoursreview.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

EventPayloadTrigger
pull_request.openedFull PR object with diff statsPR created
pull_request.synchronizePR object with new head SHANew commits pushed
pull_request.review_submittedReview object with verdictReview submitted
pull_request.commentComment object with file/line contextComment posted
pull_request.mergedPR object with merge commit SHAPR merged
pull_request.closedPR objectPR closed without merging
status_check.completedCheck result with annotationsCI 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:

MetricTypeAlert ThresholdDashboard
PR creation rateCounterAnomaly detectionOperational Overview
Merge rateCounter< 50% of creation rateFlow Dashboard
Diff generation latencyHistogramp99 > 5sPerformance Dashboard
Git clone durationHistogramp95 > 120sGit Service Health
Webhook delivery rateCounterError rate > 5%Integration Health
Notification latencyHistogramp99 > 60sNotification Dashboard
Review wait timeGaugeAvg > 24 hoursTeam Velocity
Search query latencyHistogramp95 > 2sSearch Performance
Disk usage (Git clones)Gauge> 80% capacityInfrastructure
Database connection poolGauge> 80% utilizedDatabase Health
SLOs: Our Service Level Objectives for the code review platform are: PR page load < 500ms (p95), diff rendering < 2s (p95), merge operation < 10s (p99), notification delivery < 5s in-app (p99), and 99.95% availability. These SLOs are tracked via a Prometheus-based SLO dashboard with burn-rate alerts that page on-call engineers when error budgets are at risk.

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:

ComponentSpecificationMonthly CostNotes
Web/API Servers16x c6i.2xlarge (8 vCPU, 16GB)$5,500Stateless, auto-scaling
Diff Workers8x c6i.4xlarge (16 vCPU, 32GB)$4,400Burst-capable, queue-driven
Git Service Workers16x m6i.2xlarge (8 vCPU, 32GB)$5,600Memory-heavy for Git operations
PostgreSQLdb.r6g.2xlarge Multi-AZ (2 instances)$3,200Primary + read replica
Rediscache.r6g.xlarge cluster (3 nodes)$1,500Session, diff cache, queues
Elasticsearch3x m6i.xlarge.search (32GB)$1,800PR search index
ClickHouse4x i3.2xlarge (8 vCPU, 64GB)$2,200Metrics analytics
Kafka3x kafka.m5.2xlarge$2,000Event streaming
S3 (Git objects)50TB storage + transfer$1,200Git blobs + LFS objects
CDN5TB monthly transfer$400Static assets + LFS fetch
SignalR (WebSocket)2x Standard instances$200Real-time updates
Email (SES/SendGrid)2M emails/month$500Review requests + digests
Monitoring (Prometheus/Grafana)Self-hosted on EC2$800Metrics + alerting
Total$29,300/mo~$351,600/year
Cost Optimization: Git clone workers are the most expensive component because they require large amounts of memory (32GB+) for handling big repositories. We optimize this by: (1) using shallow clones for diff generation (reduces memory by 80%), (2) sharing clones across requests via an LRU cache (30% of clones are served from cache), and (3) using Spot Instances for diff workers (reduces compute cost by 60%). These optimizations bring the effective Git infrastructure cost down by approximately 40%.

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

LayerScopeToolsCoverage Target
Unit TestsIndividual service logic (state machine, diff parsing, approval counting)xUnit, FluentAssertions, Moq90%+
Integration TestsService-to-database, service-to-Git operationsTestcontainers (PostgreSQL, Redis), LibGit2Sharp in-memory repos80%+
Contract TestsAPI contract validation between servicesPact100% API surface
E2E TestsFull PR lifecycle workflowsPlaywright (browser), REST clientCritical paths
Performance TestsDiff generation, merge operations, concurrent PRsk6, NBomberSLA 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.

Code Review & Pull Request Platform — Senior+ System Design Guide | Ayodhyya