Submodules & Monorepos
Reviewed & published by Brayan K
Sometimes one repo needs to contain another. By the end of this lesson you'll add a submodule, understand why fresh clones leave it empty, update it safely, and know when a monorepo is the simpler choice instead.
Part of the free Git course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Add a submodule with git submodule add and read .gitmodules
- Understand that the parent stores a commit pointer, not files
- Fix the empty-folder surprise with submodule update --init
- Clone everything at once with --recurse-submodules
- Update a submodule to a newer commit and record the new pointer
- Decide between submodules and a monorepo for your project
1️⃣ Adding a Submodule
git submodule add <url> <path> clones another repo into your project and registers it. Git creates a .gitmodules file (the path + URL, which you commit and share) and a special gitlink at the path that pins one exact commit.
# A submodule embeds ANOTHER Git repo inside yours, pinned to one commit.
# Useful for sharing a library across projects without copy-pasting code.
# Add a library repo into libs/dep (clones it and registers it)
git submodule add https://github.com/acme/dep.git libs/dep
# Git created two things to commit:
# .gitmodules -> records the path + URL (you commit and share this)
# libs/dep -> a "gitlink" pinning the exact commit you got
git status --short2️⃣ What Gets Committed
The parent stores a pointer to one specific commit of the submodule — not a copy of its files. That's the whole point: every teammate checks out the same version. Notice the 160000 mode in the commit output — that's Git's marker for a gitlink.
# What .gitmodules looks like (this file IS committed and shared):
# [submodule "libs/dep"]
# path = libs/dep
# url = https://github.com/acme/dep.git
# Commit both the .gitmodules file and the pinned pointer:
git commit -m "Add dep as a submodule"
# IMPORTANT: the parent repo stores a POINTER to one specific commit of
# dep — not a copy of its files. That's what guarantees every teammate
# checks out the exact same version of the library.3️⃣ The Empty-Folder Surprise
The most common submodule confusion: a plain git clone leaves submodule folders empty, because their contents aren't fetched by default. Fix it with git submodule update --init --recursive, or avoid it entirely by cloning with --recurse-submodules.
# THE classic gotcha: a normal clone leaves submodule folders EMPTY,
# because their contents aren't fetched by default.
git clone https://github.com/acme/app.git
ls app/libs/dep # 😱 empty!
# Fix it — initialise and fetch the submodule contents:
git submodule update --init --recursive
# Better: clone everything in one shot next time:
git clone --recurse-submodules https://github.com/acme/app.gitYour turn. Add a submodule and make a fresh clone fill it in. Fill in the two blanks.
# 🎯 YOUR TURN — replace each ___ then run it in a practice repo.
# Goal: add a submodule, then make a fresh clone populate it.
# 1) Add a library repo at libs/dep
git submodule ___ https://github.com/acme/dep.git libs/dep
# 👆 the subcommand that adds a submodule
# 2) Commit the .gitmodules file and the pinned pointer
git commit -m "Add dep submodule"
# 3) Someone clones the project — make their empty submodule folder fill in
git submodule update ___ --recursive
# 👆 the flag that initialises before updating
# ✅ Expected: after step 3, libs/dep contains the library's files.4️⃣ Updating a Submodule
To move a submodule forward, update it inside its folder, then commit the changed pointer in the parent. Forgetting that second commit (or to push the submodule's own new commits) is the classic way teammates end up on the wrong version.
# Updating a submodule to a newer upstream commit:
# 1) Go into the submodule and pull its latest
cd libs/dep
git fetch
git checkout main
git pull
# 2) Back in the PARENT, the pointer now shows as changed — commit it
cd ../..
git add libs/dep
git commit -m "Bump dep to latest"
# Pull a repo AND advance its submodules in one go:
git pull --recurse-submodules5️⃣ Submodules vs. Monorepos
A monorepo keeps multiple projects in one repository, so tightly-coupled code is versioned and changed together in a single atomic commit — ideal when cross-cutting changes are common. Submodules keep each project independent, linked by commit pointers — better when components have separate owners, lifecycles, or release schedules. Because submodules add ceremony and footguns, many teams reach for a monorepo (or a package manager) unless they truly need separate repositories.
Common Errors (and the fix)
- Submodule folder is empty after clone: run git submodule update --init --recursive, or clone with --recurse-submodules.
- Teammates see an old version: you moved the submodule but forgot to commit the pointer in the parent — git add libs/dep && git commit.
- "fatal: reference is not a tree" on update: you committed a pointer to a submodule commit you never pushed. Push the submodule's repo first.
- Detached HEAD inside the submodule: that's normal — submodules check out a specific commit. Run git checkout main inside it before making new commits.
- Pull doesn't update submodules: use git pull --recurse-submodules (or set submodule.recurse=true).
Pro Tips
- 💡 Auto-recurse: set git config --global submodule.recurse true so pull/checkout handle submodules for you.
- 💡 Inspect quickly: git submodule status shows each submodule's pinned commit.
- 💡 Remove cleanly: git submodule deinit then git rm the path — don't just delete the folder.
- 💡 Prefer a package manager for ordinary dependencies; reserve submodules for vendoring whole repos.
📋 Quick Reference
| Command | Purpose |
|---|---|
| git submodule add <url> <path> | Embed another repo |
| git submodule update --init --recursive | Populate submodules after clone |
| git clone --recurse-submodules <url> | Clone parent + submodules |
| git submodule status | Show pinned submodule commits |
| git pull --recurse-submodules | Pull and advance submodules |
| git submodule deinit <path> | Start removing a submodule |
Mini-Challenge: Share a Library Two Ways
No commands given this time — just the plan. Embed a library as a submodule, prove the empty-folder fix, and weigh up a monorepo.
# 🎯 MINI-CHALLENGE: share a library two ways.
# No commands given — just the plan. Use two practice repos.
#
# 1. Create a small "library" repo with one file, and commit it
# 2. In a separate "app" repo, add the library as a submodule at libs/lib
# 3. Commit the .gitmodules file and the pointer
# 4. Clone the app fresh and confirm libs/lib is EMPTY at first
# 5. Run "git submodule update --init --recursive" and confirm it fills in
# 6. Reflect: would a monorepo (both projects in one repo) be simpler here?
#
# ✅ Expected: you can populate the submodule on a fresh clone, and you can
# explain when a monorepo would have avoided the extra steps.
# your commands here🎉 Lesson Complete!
- ✅ A submodule embeds another repo pinned to one exact commit
- ✅ .gitmodules records the path/URL; the parent stores a pointer, not files
- ✅ Fresh clones leave submodules empty — update --init fills them in
- ✅ --recurse-submodules clones and pulls everything together
- ✅ Updating means committing the moved pointer in the parent
- ✅ Prefer a monorepo when code is tightly coupled and changes together
Practice quiz
What is a Git submodule?
- A branch inside a folder
- A type of merge
- A separate Git repository embedded at a fixed commit inside a parent repo
- A compressed archive
Answer: A separate Git repository embedded at a fixed commit inside a parent repo. A submodule is another repo nested in yours, pinned to a specific commit rather than copied.
Which command adds a submodule?
- git submodule add <url> <path>
- git add submodule <url>
- git clone --sub <url>
- git module add <url>
Answer: git submodule add <url> <path>. git submodule add <url> <path> registers and clones the submodule into the given path.
What file records a repo's submodules?
- .gitignore
- .submodules
- .git/config only
- .gitmodules
Answer: .gitmodules. The .gitmodules file (committed to the repo) lists each submodule's path and URL.
What exactly does the parent repo store for a submodule?
- A full copy of the files
- A pointer to one specific commit of the submodule
- Just the URL
- Nothing
Answer: A pointer to one specific commit of the submodule. The parent pins the submodule to an exact commit (a 'gitlink'), so everyone gets the same version.
After cloning a repo with submodules, why are the submodule folders empty?
- Submodule contents aren't fetched by default — you must init/update them
- A bug
- They were deleted
- They need a rebuild
Answer: Submodule contents aren't fetched by default — you must init/update them. A plain clone leaves submodules uninitialized; their files arrive only after init/update.
Which command initializes and fetches submodule contents after a clone?
- git submodule fetch
- git pull --sub
- git submodule init && git submodule update
- git checkout --sub
Answer: git submodule init && git submodule update. git submodule update --init (optionally --recursive) fills in the submodule files.
How do you clone a repo AND its submodules in one step?
- git clone --all
- git clone --recurse-submodules <url>
- git clone --deep
- git clone --modules
Answer: git clone --recurse-submodules <url>. git clone --recurse-submodules clones the parent and populates submodules together.
To move a submodule to its latest upstream commit, you...
- delete and re-add it
- run git merge
- edit .gitmodules
- cd into it, pull, then commit the new pointer in the parent
Answer: cd into it, pull, then commit the new pointer in the parent. You update the submodule, then commit the changed pointer in the parent so the new commit is recorded.
When is a monorepo often preferred over submodules?
- Never
- When tightly-coupled code should be versioned and changed together atomically
- Only for tiny repos
- Only for binaries
Answer: When tightly-coupled code should be versioned and changed together atomically. A monorepo keeps interdependent projects in one repo so cross-cutting changes land in a single commit.
A common pain point of submodules is...
- They run too fast
- They can't be cloned
- It's easy to forget to commit/push the updated submodule pointer
- They delete the parent
Answer: It's easy to forget to commit/push the updated submodule pointer. Forgetting to commit the moved pointer (or to push the submodule) leaves teammates on the wrong commit.
Continue this course
- Previous: Intro to GitHub Actions (CI/CD)
- Next: Git Hooks & Automation — Run checks automatically with pre-commit and commit-msg hooks (and husky)
- Quick reference: Git cheat sheet › .gitignore & Misc
Frequently asked questions
What is a submodule, in plain terms?
It's a way to embed one Git repository inside another. The parent repo doesn't copy the other project's files — instead it stores a pointer to one exact commit of that other repo (a 'gitlink'), plus a .gitmodules file recording the path and URL. This pins every collaborator to the same version of the embedded code, which is great for sharing a library across several projects.
Why are my submodule folders empty after cloning?
Because a normal git clone does not fetch submodule contents by default — it only records that they should exist. Run git submodule update --init --recursive to download and check them out, or clone with git clone --recurse-submodules next time to do it all at once. This 'empty folder' surprise is the single most common submodule confusion.
How do I update a submodule to a newer version?
Go into the submodule directory, fetch and check out the commit/branch you want (git fetch, git checkout main, git pull), then go back to the parent repo where the pointer now appears modified, and git add + git commit it. That commit records the new pointer so everyone else gets the same updated version. Crucially, also push the submodule's repo if you authored new commits there.
What's the difference between submodules and a monorepo?
Submodules keep each project in its own repository and link them by commit pointers — good when components have separate lifecycles, owners, or release schedules. A monorepo puts multiple projects in a single repository, so tightly-coupled code is versioned and changed together in one atomic commit. Choose a monorepo when cross-cutting changes are common; choose submodules when projects are genuinely independent.
What are the main downsides of submodules?
They add ceremony and footguns: it's easy to forget to commit the moved pointer or to push the submodule's own commits, leaving teammates stuck on an old version. Branch switching, merges, and CI all need submodule-aware commands (--recurse-submodules). Because of this friction, many teams prefer a monorepo or a package manager unless they specifically need independent repositories.