Skip to content
C Codeloom
Git

Git Submodules vs Subtrees Explained

Compare git submodules and subtrees for managing nested repositories, including workflows, trade-offs, and when to choose each approach in practice.

·4 min read · By Codeloom
Intermediate 8 min read

What you'll learn

  • How submodules pin external repos by commit
  • How subtrees embed history into the parent
  • Workflows for cloning and updating each
  • Trade-offs in CI, contributors, and tooling
  • When to prefer one over the other

Prerequisites

  • Familiar with shell
  • Basic Git branching and remotes

What and Why

Real projects rarely live in isolation. You may need a shared design system, a vendor SDK, or a private utility library used by several services. Git offers two built-in mechanisms for referencing another repository inside yours: submodules and subtrees. They solve the same problem—composing code from multiple repos—but with very different mechanics.

A submodule is a pointer: the parent repo records the URL and the exact commit SHA of the child repo at a path. The child stays a separate repo on disk. A subtree is a merge: the child’s history is folded into the parent so files live as ordinary tracked content with no special metadata.

Mental Model

Think of submodules as symbolic links with version pinning. The parent says “at libs/sdk you’ll find commit abc123 from git@github.com:org/sdk.git.” Anyone cloning must initialize submodules to actually fetch the files.

Subtrees are more like vendoring: the SDK’s files and (optionally) its history become part of the parent. A clone of the parent gets everything in one shot, with no extra commands.

This single difference cascades into every other trade-off: clone UX, contributor experience, CI configuration, and how you push fixes upstream.

Hands-on Example

Let’s add a shared library common-utils to a parent app using both approaches so you can see the contrast.

# Submodule workflow
git submodule add git@github.com:org/common-utils.git libs/utils
git commit -m "Add common-utils submodule"

# Clone later
git clone git@github.com:org/app.git
cd app
git submodule update --init --recursive

# Update the pinned commit
cd libs/utils
git fetch && git checkout v1.4.0
cd ../..
git add libs/utils
git commit -m "Bump common-utils to v1.4.0"
# Subtree workflow
git remote add utils-remote git@github.com:org/common-utils.git
git subtree add --prefix=libs/utils utils-remote main --squash

# Pull updates
git subtree pull --prefix=libs/utils utils-remote main --squash

# Push local fixes back upstream
git subtree push --prefix=libs/utils utils-remote feature/fix-bug
Submodule layout                Subtree layout
-----------------               -----------------
app/                            app/
.gitmodules                     libs/
libs/                            utils/
  utils/  --> commit abc123        index.js
              (separate .git)      package.json
                                   (tracked files)

Parent records: URL + SHA       Parent records: real blobs
Parent repo layout: submodule stores a pointer, subtree stores actual files

In the submodule case, libs/utils has its own .git directory and behaves as an independent repo. In the subtree case, the files are indistinguishable from any other directory.

Common Pitfalls

  • Forgetting --init --recursive. New contributors clone the parent and see empty submodule directories, then file confused bug reports.
  • Detached HEAD inside submodules. git submodule update checks out a specific SHA, not a branch. Editing without first running git checkout main loses commits.
  • Subtree merge noise. Without --squash, subtree pulls bring in every upstream commit and can produce confusing merge graphs.
  • Pushing subtree changes. git subtree push rewrites history from the prefix; it can be slow on large repos and surprises contributors who expect a normal git push.
  • Mixing both in one repo. This is technically possible but multiplies cognitive overhead—pick one.

Practical Tips

  • Use submodules when the child repo is large, evolves independently, or has its own release cadence. Pinning by SHA gives you reproducibility and a clean separation of ownership.
  • Use subtrees when contributors should treat the embedded code as part of the project, when you want a single clone command, or when CI shouldn’t need extra steps.
  • Add a make bootstrap or scripts/setup.sh target that runs git submodule update --init --recursive. Newcomers will thank you.
  • For subtrees, document the remote name and prefix in CONTRIBUTING.md. Without that note, future maintainers won’t know how to pull updates.
  • Consider package managers (Go modules, npm, pip) before reaching for either. They solve the same problem with better tooling for most cases.

Wrap-up

Submodules and subtrees both let you compose repos, but they make opposite trade-offs. Submodules favor isolation and precise pinning at the cost of contributor friction. Subtrees favor a seamless clone and edit experience at the cost of history complexity and harder upstream pushes. Pick based on who edits the embedded code most often and how independently the projects evolve.