AlgoInsight

Free lesson · Git Mastery

Submodules Overview

Hard7 min readgit

Submodules Overview

A note from Olena Marchenko, Software Engineer II at Microsoft Azure DevTools.

When a Repository Needs Another Repository

A submodule is a Git repository nested inside another Git repository. The outer repository (the "superproject") tracks a specific commit of the inner repository (the "submodule"). The outer repository's history records "at this commit, the submodule was at that commit"; the actual content of the submodule lives in its own repository.

Submodules exist for a specific situation: when you need to use code from another repository within yours, but you don't want to copy the code in. You want to use it; you want to know exactly which version you're using; you want to update independently of the upstream. Submodules track this relationship at the commit level.

When I came to Microsoft Azure DevTools from Zalando, I hadn't worked with submodules at meaningful depth. My early projects had been simpler - single repositories with internal dependencies managed by package managers. The first time I encountered a serious submodule setup, on a project that integrated several internal libraries, I made every mistake there is to make with submodules. This module exists in part to spare other engineers the same learning cycle.

This topic frames what submodules are and when they make sense. Topics 15.2 through 15.5 cover the mechanics. Topic 15.8 covers the strategic comparison with subtrees.

What a Submodule Actually Is

At the data level (Topic 14.6 covered tree objects), a submodule is a special tree entry with mode 160000:

160000 commit 7a3b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b    vendor/library

The mode 160000 is the gitlink marker. The hash isn't a tree or blob in the outer repository's object store - it's a commit hash in the submodule's repository. The outer repository's tree contains a pointer to a specific commit of the submodule.

The pointer is a commit reference, not the content. The outer repository tracks which version of the submodule is expected at this path. The actual content lives in the submodule's own repository - its own objects, its own refs, its own history. The two repositories are linked by this commit reference but otherwise independent.

On disk, the submodule appears as a subdirectory of the outer repository. But that subdirectory is its own Git repository - it has its own .git/ (actually a pointer to one stored elsewhere, but conceptually the same).

The Parts of a Submodule Setup

A repository with submodules has several files and locations:

my-project/
├── .git/                       # outer repository's Git state
├── .gitmodules                 # submodule configuration (committed)
├── src/
│   └── ...
└── vendor/
    └── library/                # submodule directory
        ├── .git                # submodule's Git state (file or directory)
        └── ...                 # submodule contents

The .gitmodules file is committed to the outer repository and describes each submodule:

[submodule "vendor/library"]
    path = vendor/library
    url = https://github.com/example/library.git
    branch = main
The .gitmodules file is the registration of the submodule. It tells Git where the submodule's repository is and where in the working tree to clone it. The file is committed; everyone who clones the superproject gets this configuration.

The outer repository's tree records the commit hash of the submodule (the 160000 entry). The .gitmodules file says where to find the submodule's repository. Together, these specify "clone X from Y at commit Z."

When Submodules Make Sense

The cases where submodules are the right tool:

You need a specific version of an external dependency that isn't well-managed by package managers. Common for system libraries, build tools, or framework code where you want repository-level integration rather than package-manager dependency.

You're embedding a project that has its own lifecycle. A third-party library where you occasionally update to newer versions; an internal library shared across multiple projects.

You want to maintain a fork with upstream tracking. A submodule pointing at your fork's repository, with the original upstream as a remote of that fork. You can update from upstream while maintaining your changes.

You're working with multi-repository products where some repositories are sources for others. The outer repository assembles components from multiple sources; submodules track the components.

When Submodules Don't Make Sense

The cases where submodules are usually the wrong tool:

The dependency is well-managed by your language's package manager. Don't make a submodule out of something npm or pip or Cargo handles fine. The package manager is built for that purpose.

You need to make frequent changes to the submodule from the superproject's workflow. Submodules add friction - every change requires committing in the submodule, then committing the new pointer in the superproject. If this happens daily, the friction compounds.

The team is unfamiliar with submodules. The learning curve is real. A team without submodule experience may struggle with cloning, updating, and managing the lifecycle.

You're using submodules to avoid making decisions about ownership. If "we put it in a submodule because we didn't want to decide" is the rationale, the decision usually wasn't avoided - it was deferred at the cost of complexity.

The discipline I've learned: submodules are powerful but expensive. Use them when other tools genuinely don't fit, not as a default.

A Visualization

graph TD
    subgraph SP["Superproject (my-project)"]
        SPC[commit a3f2c1d]
        SPT[tree]
        GM[.gitmodules<br/>url + path]
        GL[gitlink entry<br/>vendor/library = 7a3b2c4d]
    end
    
    subgraph SM["Submodule repository (library)"]
        SMC[commit 7a3b2c4d]
        SMH[commit history]
    end
    
    SPC --> SPT
    SPT --> GM
    SPT --> GL
    GL -.references.-> SMC
    SMC --> SMH
    
    style SP fill:#dbeafe
    style SM fill:#dcfce7
The diagram shows the relationship. The superproject's tree contains a regular file entry for .gitmodules and a special gitlink entry for the submodule path. The gitlink references a specific commit in the submodule's separate repository. The two repositories have their own histories that intersect only at the pinned commit.

A Pattern at Microsoft

The Azure DevTools team uses submodules for a few specific internal libraries that are shared across multiple developer-tooling repositories. The pattern:

The discipline is explicit. Submodule updates are deliberate; they don't drift. Engineers know exactly which version of each library their build is using. Reproducible builds depend on this - if you check out a commit of the superproject, you get exactly the submodule versions that commit recorded.

A Specific Story From My Zalando Days

At Zalando, I worked on a project that had three submodules. I'd never used submodules before; the documentation said to clone with --recurse-submodules; I cloned without the flag because I missed it.

The result: the project appeared to clone successfully. The submodule directories existed. They were empty. The build immediately failed with confusing errors about missing files.

I spent about an hour confused before a senior engineer pointed out the issue. The fix was running git submodule update --init --recursive to populate the submodules. Topic 15.3 covers cloning with submodules in detail.

The lesson stuck. Submodules require explicit operations to manage; default git commands don't automatically handle them. Engineers new to submodules need to learn this; the failure mode (silent empty directories) is confusing the first time you encounter it.

A Common Misconception

A pattern I've seen across multiple teams: engineers thinking that committing in the superproject also commits submodule changes. It doesn't.

Submodule changes have to be committed in the submodule first. Then the superproject's pointer to the submodule has to be updated and committed in the superproject. Two separate commits, in two separate repositories.

# Inside the submodule
cd vendor/library
# ... make changes ...
git add .
git commit -m "Submodule changes"
git push                                # push to the submodule's repository

# Back in the superproject
cd ../..
git add vendor/library                  # stages the new submodule pointer
git commit -m "Update vendor/library to include changes"
git push
The two-step commit is what catches engineers who don't understand the structure. Changes inside the submodule directory are changes to the submodule, not to the superproject. The superproject only sees that the submodule is at a different commit than before; updating the pointer is the superproject's commit.

This is why submodules have higher friction than alternative approaches like subtrees (Topic 15.6) or monorepos.

What Comes Next

Topic 15.2 covers adding a submodule. Topic 15.3 covers cloning a repository that has submodules. Topic 15.4 covers updating submodule pointers. Topic 15.5 covers removing submodules. Topics 15.6 and 15.7 cover the subtree alternative. Topic 15.8 covers the strategic decision between submodules and subtrees.

What Goes Wrong

Warning: Common submodule misconceptions:

Thinking submodules are like nested folders. They're separate repositories - every operation respects that.

Forgetting to clone with --recurse-submodules. Empty submodule directories with confusing build errors.

Expecting git commit in the superproject to commit submodule changes. Two commits, two repositories.

Using submodules when a package manager would suffice. Adds complexity for no benefit.

Not pinning to specific commits. Floating pointers break reproducibility.

Ignoring submodule discipline because "we'll learn it later." Mistakes compound; better to learn the patterns up front.

Try It

Exercise: Explore submodules:

  1. Find a public repository that uses submodules. Look at its .gitmodules file.
  2. Look at a 160000 tree entry: git ls-tree HEAD in a repo with submodules.
  3. Run git submodule status to see the current submodule state.
  4. In a test repository, add a submodule (Topic 15.2 covers the mechanics).
  5. Inspect the submodule's .git directory - is it a file pointer or a directory?
  6. Look at the superproject's commit log. Does it show submodule changes? (It shows the pointer updates.)

Back to Git Mastery