Monorepos, Sparse Checkout & Worktrees
As a company grows, it has to decide where its code lives. Some teams keep every app and service in one big repository, a monorepo. Others give each project its own repository. Both work, but a monorepo brings a practical problem: cloning and checking out everything becomes slow when you only need one folder.
In this lesson you will compare the main ways to organise many projects, then learn the Git features that make a large repository comfortable: partial clones, shallow clones, sparse checkout and worktrees. You will finish with CI and code ownership rules for a monorepo.
Monorepo, polyrepo or submodules
Imagine a food delivery start-up in Pune with a customer website, a mobile app, a payments service, an orders service and a shared UI library.
| Approach | What it looks like | Strengths | Pain points |
|---|---|---|---|
| Monorepo | One repository with apps/, services/ and packages/ folders | One pull request can change the API and the app together; shared code is always in sync; one set of tools | Repository grows large; CI must avoid testing everything on every change |
| Polyrepo | One repository per app or service | Small, fast clones; each team controls its own repository | A change across services needs several pull requests in the right order; shared code is versioned and published separately |
| Submodules | A main repository that pins other repositories at exact commits | Reuse a separate repository without copying it | Extra commands, easy to forget to update; see Submodules |
There is no single right answer. Monorepos suit teams that change many parts together; polyrepos suit independent teams with separate release cycles. The rest of this lesson is about making a monorepo fast.
Here is the layout of the start-up's shop-platform monorepo:
shop-platform/
apps/web/
apps/mobile/
services/payments/
services/orders/
packages/ui/
docs/
package.jsonPartial clone: skip old file contents
A normal clone downloads every version of every file ever committed. A partial clone downloads all the commits and folder listings but leaves file contents (blobs) behind until you actually need them. The usual filter is blob:none:
git clone --filter=blob:none --sparse https://github.com/meera-kulkarni/shop-platform.git web-devGit fetches the missing contents automatically, on demand, when a command needs them: checking out a branch, running git diff on an old commit or git blame. You keep the full history for git log, but the first download is much smaller. This works on GitHub and most hosting services; the server must allow filtering.
The --sparse option in that command starts the next feature: only the files in the root folder are checked out at first.
Sparse checkout: only the folders you work on
Sparse checkout keeps the whole project in the repository but puts only some folders on your disk. Meera works on the website, which uses the shared UI library. Right after the sparse clone above, her working tree holds just the root files:
cd web-dev
ls -A
git sparse-checkout add apps/web packages/ui
ls -A
ls apps
git sparse-checkout list.git package.json .git apps package.json packages web apps/web packages/ui
apps contains only web; the mobile app and the services are not on her disk, yet commits, branches and pushes work normally. git sparse-checkout set replaces the whole list instead of adding to it, and git sparse-checkout disable brings every file back.
The folder list uses cone mode, which is the default. In cone mode you name directories, and Git includes everything inside them plus the files directly in their parent folders, such as the root package.json. Cone mode is fast even in huge repositories. Older guides use a non-cone mode with .gitignore-style patterns; the official documentation does not recommend it.
Shallow clones for CI
A shallow clone cuts history instead of file contents. --depth 1 downloads only the latest commit, which is ideal for a CI job or a deployment that just builds the current code:
git clone --depth 1 https://github.com/meera-kulkarni/shop-platform.git ci-copy
git -C ci-copy log --onelineCloning into 'ci-copy'... 930498c (grafted, HEAD -> main, origin/main, origin/HEAD) Update web and payments (3)
grafted means Git knows this commit has parents but did not download them. Commands that need older history, such as git log beyond that commit, git blame and git bisect, will not work fully. For daily development prefer a partial clone, which keeps the history. GitHub Actions' actions/checkout step makes a shallow clone with depth 1 by default.
Worktrees: two branches checked out at once
You are halfway through a feature when a production bug arrives. Instead of stashing or cloning the whole monorepo again, git worktree gives you a second working folder that shares the same repository. Kabir is on feature/festival-offers with uncommitted changes:
git worktree add ../shop-hotfix -b hotfix/upi-timeout main
git worktree listPreparing worktree (new branch 'hotfix/upi-timeout') HEAD is now at 930498c Update web and payments (3) /home/kabir/shop-platform 930498c [feature/festival-offers] /home/kabir/shop-hotfix 930498c [hotfix/upi-timeout]
He fixes the bug in ../shop-hotfix, commits there and pushes. His feature folder is untouched, uncommitted change and all. When the hotfix is done, he removes the extra folder:
git worktree remove ../shop-hotfix
git branch* feature/festival-offers hotfix/upi-timeout main
The worktree is gone but the branch and its commit remain. One rule: a branch can be checked out in only one worktree at a time, so Git refuses to open main in two folders.
CI and ownership in a monorepo
Running every test for every change wastes time. GitHub Actions can start a workflow only when files in certain folders change, using paths filters:
name: Payments service
on:
pull_request:
paths:
- "services/payments/**"
- "packages/ui/**"
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: npm ci
- run: npm test --workspace=services/paymentsA website-only pull request now skips the payments tests. Be careful when such a workflow is a required check: GitHub documents that a workflow skipped by a path filter leaves its check pending, which can block the pull request. See GitHub Actions: First CI Workflow for the basics of workflow files.
Each team should also review changes to its own folder. A CODEOWNERS file maps folders to reviewers, for example /services/payments/ @shop-platform/payments-team. GitHub then requests that team's review automatically. Branch Protection, Rulesets & CODEOWNERS covers the syntax and how to make those reviews required.
Common mistakes
- Using
--depth 1for daily work. History-based commands break. Use--filter=blob:noneinstead and keep shallow clones for CI. - Thinking sparse checkout limits access. Every file is still in the repository and visible to anyone who can clone it. It only saves disk space and time.
- Deleting a worktree folder by hand. Git keeps a record of it. Use
git worktree remove, or rungit worktree pruneafter an accidental delete. - One giant CI job for the whole monorepo. Split workflows by folder with
pathsfilters so a typo fix indocs/does not run every test. - Choosing a monorepo only because big companies use one. They also have tooling teams. Pick the layout that fits how your teams change code together.
Key points
- A monorepo keeps many projects in one repository; polyrepos and submodules are the alternatives.
git clone --filter=blob:none(partial clone) keeps full history but downloads file contents only when needed.--depth 1(shallow clone) fetches only recent commits and suits CI, not daily development.git sparse-checkout setoraddin cone mode puts only chosen folders on disk.git worktree addchecks out another branch in a second folder that shares the same repository.- Use
pathsfilters in workflows andCODEOWNERSentries per folder to keep a monorepo's CI and reviews focused.
Exercise
Create a practice monorepo with the folders apps/web, apps/admin and services/api, each with a README.md, and commit it. Then:
- Clone it a second time with
--sparseso only root files appear. - Add only
apps/adminto the sparse checkout and confirm withls. - In the original repository, open a worktree for a new branch
fix/login-textin a sibling folder, make a commit there, list worktrees and remove it.
Show solution
git clone --sparse monorepo-practice sparse-copy
cd sparse-copy
git sparse-checkout add apps/admin
ls apps
cd ../monorepo-practice
git worktree add ../login-fix -b fix/login-text
git -C ../login-fix commit --allow-empty -m "fix: correct login button text"
git worktree list
git worktree remove ../login-fixls apps shows only admin. git worktree list shows two folders until the remove, and the fix/login-text branch with its commit survives the removal. (A local clone ignores --filter, so it is left out here; use it when cloning from a server.)
Quick Quiz
1. Which clone keeps the full commit history but downloads file contents only when they are needed?
A blobless partial clone has every commit and tree; blobs are fetched on demand.
2. What does git sparse-checkout set apps/web change?
Sparse checkout affects only which files are on your disk; the repository still has everything.
3. Why is a shallow clone a poor choice for everyday development?
A shallow clone lacks older commits, so history-based commands are limited.
4. You need to fix a bug on main while your feature branch has uncommitted work. Which command avoids stashing?
A worktree gives a second folder with its own checked-out branch, sharing the same repository.
Learn Git with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
