The Hook (The "Byte-Sized" Intro)
Submodules are powerful but opinionated. Misuse them and they become the most hated part of your workflow — mysterious empty directories, detached HEADs, and builds that only work on your machine. Follow these practices and submodules stay predictable, maintainable, and actually useful.
📖 What are Submodule Best Practices?
Guidelines for using Git submodules effectively without creating maintenance headaches.
Conceptual Clarity
The 7 rules:
| # | Rule | Why |
|---|---|---|
| 1 | Pin to stable commits or tags | Don't track HEAD — it's unpredictable |
| 2 | Update intentionally | Review changes before moving the pointer |
| 3 | Document initialization | New devs need clear setup instructions |
| 4 | Use submodule.recurse true | Automates init/update on pull and clone |
| 5 | Never commit inside a submodule | Make changes in the submodule's own repo |
| 6 | Keep submodule count low | Each submodule adds complexity |
| 7 | Consider alternatives first | Package managers may be simpler |
When to use submodules vs alternatives:
| Use Case | Best Approach |
|---|---|
| Shared library with its own release cycle | ✅ Submodule |
| Third-party dependency | ❌ Package manager (npm, pip) |
| Monorepo with linked projects | ❌ Monorepo tools (Nx, Turborepo) |
| Shared config files | 🟡 Submodule or template repo |
| Vendor code you don't modify | ✅ Submodule or subtree |
Real-Life Analogy
Submodules are like LEGO sets with compatible pieces from another set. They work great if you follow the instructions and keep versions aligned. Force-fit random pieces and the whole thing falls apart.
Visual Architecture
Why It Matters
- Predictability: Pinning to tags means everyone gets the same code.
- Onboarding: Clear docs prevent the "empty directory" confusion.
- Simplicity: Fewer submodules = less maintenance overhead.
- Alternatives: Package managers solve most dependency problems better.
Code
# ─── Pin to a tagged release (not HEAD) ───
cd libs/shared-utils
git checkout v2.1.0
cd ..
git add libs/shared-utils
git commit -m "chore: pin shared-utils to v2.1.0"
# ─── Enable automatic submodule handling ───
git config --global submodule.recurse true
# ─── Document in README ───
# ## Setup
# ```bash
# git clone --recurse-submodules <repo-url>
# # Or if already cloned:
# git submodule update --init --recursive
# ```
# ─── Review before updating ───
cd libs/shared-utils
git log --oneline HEAD..origin/main # See what's new
git diff HEAD..origin/main --stat # See changed files
# Only update if changes are relevant and testedKey Takeaways
- Pin to tags, not HEAD — predictable builds matter.
- Update intentionally — review changes before moving the pointer.
- Document setup — include
--recurse-submodulesin your README. - Consider alternatives — package managers are simpler for most dependencies.
Interview Prep
-
Q: When should you use submodules vs a package manager? A: Use submodules for shared libraries with their own release cycle that you need to pin to specific commits. Use package managers (npm, pip) for third-party dependencies — they handle versioning, caching, and updates better.
-
Q: Why shouldn't you make commits directly inside a submodule? A: Because the submodule is a separate repository. Changes should be made, tested, and pushed in the submodule's own repo, then the parent repo updated to point to the new commit. Committing inside the submodule from the parent can lead to detached HEAD confusion.
-
Q: What is the risk of tracking a submodule's HEAD instead of a tag? A: HEAD changes every time someone pushes to the submodule. Your builds become non-reproducible because different developers may get different versions depending on when they update.