The Blueprint
Infrastructure advanced

Design GitHub

Git protocol underneath, a metadata database on top, and a review/CI product wrapped around both — three planes that scale independently.

asked at GitHub · GitLab · Atlassian Sometimes asked ~40 min walkthrough ✎ Practice on canvas →
01

Scope the requirements

Draw the seams: git object storage (write-once, cold), metadata (PRs, issues, users — transactional), and derived views (search, notifications, activity feeds). Each has different SLAs.

Functional — what it must do

  • Host git repos: clone, push, pull, fetch — including large monorepos.
  • Pull requests: branches, review threads, merge with strategies.
  • Issues + comments with markdown + mentions.
  • Code search across all public repos.
  • Explicitly cut: Actions/CI, packages, wiki — offer them back.

Non-functional — the numbers that shape the design

  • Clone latency for a warm 1 GB repo under 10 s end-to-end.
  • 500M repos, ~100M active users, ~10k git pushes/s peak.
  • Repo storage: petabytes. Access pattern is highly skewed — Linux kernel gets thousands of clones/day, most repos get zero.
  • Search index over ~10B code files.
  • 99.99% availability on read (clone/pull); write can retry.
Say this out loud

Git is the hard part because the protocol has weird properties — content-addressed, cold, packed. Everything else is normal web scale.

02

Back-of-envelope estimates

Storage is petabyte-scale but bandwidth is modest for git operations. Search is what pushes hardware.

QuantityEstimateHow you got there
Repos~500Mpublic + private. Long-tail distribution; top 0.1% get 99% of the traffic.
Git ops/s~50k clones + 10k pushes peakclones dominate; pushes small but expensive (fsync + pack rebuild).
Storage~500 PBavg repo ~1 GB (skewed by monorepos); deduped across forks via shared objects.
Code files~10Bsearch index rebuilt continuously; ~50 TB of postings + ngrams.
Metadata rows~50Bissues, PRs, comments, reviews. Postgres cluster per shard.
Push egress~1 TB/s during release windowsbig projects pushing tags trigger CI clones from thousands of workers.
Say this out loud

Storage is petabytes but read bandwidth is bounded — most reads hit git object cache, not disk. The real design question is packing and fork dedup.

03

Sketch the API

Git protocol on port 22 (SSH) or 443 (HTTPS); REST API for everything else. GraphQL sits on top of REST for expressive queries.

GETgit+https://github.com/{org}/{repo}.git/info/refs?service=git-upload-packGit smart HTTP — clone/fetch handshake.
POSTgit+https://github.com/{org}/{repo}.git/git-upload-packServer sends packfile — the actual clone data.
POSTgit+https://github.com/{org}/{repo}.git/git-receive-packPush — client sends packfile, server updates refs.
GET/api/v3/repos/{owner}/{repo}/pulls/{n}→ PR details from metadata store.
POST/api/v3/repos/{owner}/{repo}/pulls/{n}/mergeMerge PR — runs the merge strategy + updates refs.
GET/api/v3/search/code?q=Code search over indexed files.
  • Git protocol is bespoke: not HTTP semantics you can layer conventional caching on. Custom router picks the git backend holding the repo's shard.
  • Merge is done server-side — the API applies the client's requested strategy (merge, squash, rebase) against fresh state, not the state the PR was opened against.
04

Data model

Two large storage systems: git object store (content-addressed, immutable) and metadata database (transactional). Search is a derived index over both.

git_objects
sha PK · type · content (blob)
content-addressed key-value; blobs, trees, commits, tags. Immutable, deduped across repos.
refs
(repo_id, ref_name) PK · sha · updated_at
the only mutable git state — branch and tag pointers. Small table, hot on push.
repos
id PK · owner_id · name · shard_id · created_at
Postgres. Sharded by hash(owner_id) so a user's repos live together.
prs / issues / comments
id PK · repo_id · author_id · body · created_at
Postgres per-shard. Followed by CDC into search + notifications.
code_index
term → [file_id]
purpose-built search index (Elasticsearch or hand-rolled). Sharded by repo_id hash.
The database call

The git object store is content-addressed — a custom layer on top of local disk + object storage. Refs go in a fast KV (Redis-backed with WAL for durability). Everything else is Postgres with CDC into Elasticsearch.

05

High-level design

Three tiers: an edge router that picks the git backend for a given repo, the git backend itself (holds objects + refs), and the metadata/web tier that handles PRs/issues.

client edge service data async git client Browser SSH front HTTPS LB API gateway Git router repo → shard Git backend packs, refs Meta svc PRs, issues Object store content-addressed Refs store Postgres metadata CDC Elasticsearch Event bus
git router picks the shard holding a repo; git backend holds refs + objects. metadata sits on postgres with cdc into es.
  1. Clone: client → SSH/HTTPS → router looks up which shard holds owner/repo → forwards to that git backend.
  2. Git backend reads refs from the fast store, computes packfile of missing objects for the client, streams the bytes.
  3. Push: client sends packfile → git backend verifies + writes new objects (immutable content-addressed) → atomically updates the ref.
  4. PR create: metadata service writes a row → CDC publishes to Kafka → search indexer updates ES + notifier fans out to subscribed users.
  5. Merge: metadata service checks CI status + reviews → asks git backend to perform the merge strategy on server side (produces a merge commit or squashed commit) → updates the target ref → posts merge event.
06

Deep dives — where the interview is won

Phase 01

Git storage: content-addressed dedup across forks

Every git object (blob, tree, commit) is named by the SHA-1 of its content. Identical content = same SHA = one copy. When you fork Linux, you don't copy 5 GB of history — you share the object store with the source repo and only add new objects when you push. This is why storage grows sub-linearly with repos.

The backend keeps 'alternates' — a config line pointing each fork's local store at the parent's shared pool. On clone, the packfile is composed from both. On push, only new objects go to the fork's own store. GitHub's storage bill would be astronomical without this — real repos with many forks share 90%+ of their objects.

forks shared own Repo A Fork of A Fork of A Shared object pool A own B own C own
shared pool holds the parent's objects. each fork only stores its diverging objects.
Trade-offDeleting a source repo becomes hard — forks depend on its object store. GitHub solves this by promoting objects to a shared 'network' pool that outlives any single repo.
Phase 02

Sharding a repo across a git farm without breaking clone locality

Repos vary in size by 5 orders of magnitude. A naive random-shard scheme puts a Linux-kernel-sized repo on one box that gets crushed. The design: assign each repo to a shard at creation (hash of owner_id gives locality — a user's repos co-locate), then move hot repos to dedicated shards proactively.

For very large monorepos, replicate across N read shards — clones hit any of them, pushes go through a leader with sync replication. The router (a small stateless service) reads a repo → shard map from a global config store and routes accordingly.

client router git farm Clone request Router repo→shard map Shard A Hot repo shard replicated Shard N
hot repos get dedicated replicated shards. cold repos share shards by locality.
Trade-offLive repo migration between shards is complex — you have to freeze pushes briefly. Do it during scheduled maintenance for hot repos; small repos migrate in the background.
Phase 03

Code search over ~10B files without scanning them all

Grep across 500 PB is not a plan. Code search uses an inverted index: for every trigram (three-character sequence) in a file, record the file id. A search for func_name becomes a set intersection of trigram posting lists — fast because trigram sets are small per file and posting lists are compact.

The catch: code has more distinct trigrams than natural language (all those ___ ->s foo combinations). Index storage is ~5-10× the raw code. Sharding by repo_id keeps search results consistent for a single-repo query; global search fans out to all shards and merges.

query search shards source GET /search?q=x Shard 1 trigrams Shard 2 Shard N Blob store snippet fanout
trigram index shards return file ids; source blobs fetched only for the visible snippets.
Trade-offIndex freshness lag: new commits take seconds to minutes to appear in search. Acceptable — code search is exploratory, not real-time. Streaming updates via CDC keeps lag bounded.
07

Follow-ups you should expect

What happens on a force-push to a widely-cloned repo?
Ref pointer moves; old objects become unreachable but not deleted (git's reflog + gc semantics). Clients that had the old ref get an error on next fetch and must reset. No corruption; some CI systems that referenced the old SHA might fail — GitHub warns on force-push to protected branches.
Merge conflicts — where does the resolution happen?
Server-side merge attempts the strategy first; if trivial, produces a merge commit and updates the target ref. If conflicts, the merge fails and the PR shows 'conflicts — resolve locally'. GitHub does NOT do a UI-based conflict editor for anything beyond trivial line-level conflicts — the client's git is more capable.
How do you scale from 100M repos to 1B?
Add shards. Because repos are already sharded by hash, existing repos don't move — only new repos land on new shards. The metadata layer scales by adding Postgres shards on the same hash. The bottleneck at that scale is often the search index, which needs re-partitioning by activity to keep hot repos on faster boxes.
How does the search index stay fresh on 10k pushes/s?
Push emits a ref_updated event to Kafka. Indexer consumes, fetches the diff between old and new ref via the git backend, applies incremental updates to trigrams (add new files, remove deleted ones). Full re-index of a repo happens on schedule (weekly) and after major merges.
What if a git backend shard dies?
Reads fail over to a replica (if present). Pushes to that shard fail until the shard is recovered — a git push is expected to sometimes fail, and clients retry. Recovery from replica is fast because git objects are content-addressed; you re-sync missing objects and refs from the surviving replica.

Where candidates lose the room

  • Storing every fork as a full copy — dedup via shared object pools is the whole storage story.
  • Randomly sharding repos with no locality — a user's dashboard fans out to every shard for a listing query.
  • Doing merges client-side and only accepting the resulting SHA — the server needs to enforce merge rules against fresh state, not the state when the PR opened.
  • Full re-scan on code search — trigram indexes are the difference between milliseconds and seconds.
  • Skipping the packfile compression — git without packing is 5-10× larger on disk and slower to clone.

Concepts this leans on

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.