When working on a product with multiple independent repositories, it is common to have separate repositories for the backend, frontend, mobile application, smart contracts, admin panel, or shared services.
For example:
Product Master Repository
│
├── backend
├── frontend
├── mobile
├── contracts
└── admin
One approach is to combine everything into a monorepo. Another approach is to keep each project as an independent repository and use Git submodules to bring them together under a master repository.
Git submodules are useful when you want to maintain repository independence while still having a single place that represents the complete product.
This article explains how Git submodules work, how to manage branches, how to update repositories, and the commands I use most often.
What is a Git Submodule?
A Git submodule is a Git repository embedded inside another Git repository.
Imagine you have these independent repositories:
github.com/example/backend
github.com/example/frontend
github.com/example/mobile
github.com/example/contracts
You can create another repository:
product-master/
├── backend/
├── frontend/
├── mobile/
└── contracts/
The directories are not ordinary folders. Each one is connected to its own Git repository.
The master repository simply keeps track of which commit of each submodule should be used.
For example:
product-master
│
├── backend → commit 82a31f
├── frontend → commit 71bc92
├── mobile → commit 19cd83
└── contracts → commit 52de11
This is an important concept.
The master repository does not store the complete Git history of those repositories. It stores references to specific commits.
Why Use Git Submodules?
Git submodules are particularly useful when repositories need to remain independent.
For example, a company might have:
Platform
├── API
├── Web Application
├── Mobile Application
├── Admin Dashboard
├── Smart Contracts
└── Notification Service
Each project may have:
- Its own development team
- Its own release cycle
- Its own CI/CD pipeline
- Its own branch strategy
- Its own GitHub permissions
- Its own deployment process
At the same time, having a master repository can make it easier to understand the complete product structure.
This gives you a hybrid architecture:
Master Repository
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Backend Frontend Contracts
│ │ │
Git Repo Git Repo Git Repo
Creating a Master Repository
First create your master repository normally:
mkdir product-master
cd product-master
git init
git remote add origin https://github.com/example/product-master.git
Create your initial commit:
git add .
git commit -m "Initial commit"
git push -u origin main
Now you can start adding independent repositories.
Adding a Repository as a Submodule
The basic syntax is:
git submodule add <repository-url> <directory>
For example:
git submodule add https://github.com/example/backend.git backend
Another repository:
git submodule add https://github.com/example/frontend.git frontend
And another:
git submodule add https://github.com/example/mobile.git mobile
Your structure becomes:
product-master/
├── backend/
├── frontend/
└── mobile/
Git also creates a file called:
.gitmodules
Understanding .gitmodules
The .gitmodules file contains the configuration of your submodules.
It may look like:
[submodule "backend"]
path = backend
url = https://github.com/example/backend.git
[submodule "frontend"]
path = frontend
url = https://github.com/example/frontend.git
[submodule "mobile"]
path = mobile
url = https://github.com/example/mobile.git
The important properties are:
path— where the repository exists inside the master repositoryurl— the actual Git repository
Commit this file:
git add .gitmodules
git commit -m "Add project repositories"
git push
Cloning a Repository with Submodules
This is one of the most important commands to remember.
Instead of:
git clone https://github.com/example/product-master.git
use:
git clone --recurse-submodules https://github.com/example/product-master.git
Git will clone the master repository and initialize all its submodules.
If you already cloned the master repository without submodules, run:
git submodule update --init --recursive
This will initialize and download the submodules.
Checking Submodule Status
To see all submodules:
git submodule
You can also use:
git submodule status
You might see:
82a31f backend
71bc92 frontend
19cd83 mobile
These values are the commit SHAs currently referenced by the master repository.
The Most Important Concept: Submodules Track Commits
This is probably the most confusing part when first working with submodules.
Suppose the backend repository has:
main
│
├── A
├── B
├── C
└── D
The master repository might currently point to:
backend → commit C
Even if commit D exists in the backend repository, the master repository still points to C.
This is intentional.
It means the master repository can represent an exact version of the entire product.
For example:
Product Release 2.0
Backend → 82a31f
Frontend → 71bc92
Mobile → 19cd83
Contracts → 52de11
Someone can clone that master repository six months later and get exactly those versions.
This makes submodules particularly useful for release management.
Switching Branches Inside a Submodule
A submodule is a real Git repository.
You can enter it:
cd backend
Check branches:
git branch -a
Switch to another branch:
git switch develop
Or:
git switch staging
You can work with it exactly like any other Git repository.
For example:
git switch develop
git pull origin develop
Make changes:
git add .
git commit -m "Implement new feature"
git push origin develop
Then return to the master repository:
cd ..
An Important Detail When Switching Branches
Suppose the master repository currently points to:
backend → commit A
You enter the backend repository and switch to a branch containing:
commit B
The master repository will now see the submodule as changed.
Run:
git status
You may see:
modified: backend (new commits)
This happens because the master repository was pointing to commit A, but the submodule is now at commit B.
If you want the master repository to use commit B:
git add backend
git commit -m "Update backend version"
git push
The master repository now points to B.
Configuring a Branch for a Submodule
You can tell Git which branch a submodule should follow.
For example:
[submodule "backend"]
path = backend
url = https://github.com/example/backend.git
branch = develop
Now:
git submodule update --remote backend
will use the configured branch when updating the submodule.
You can configure different branches for different repositories:
[submodule "backend"]
path = backend
url = https://github.com/example/backend.git
branch = develop
[submodule "frontend"]
path = frontend
url = https://github.com/example/frontend.git
branch = staging
[submodule "contracts"]
path = contracts
url = https://github.com/example/contracts.git
branch = main
This allows:
Backend → develop
Frontend → staging
Contracts → main
Updating a Single Submodule
If you only want to update the backend:
git submodule update --remote backend
You don't have to update all submodules.
For example:
backend → update
frontend → unchanged
mobile → unchanged
contracts → unchanged
After updating:
git status
Then commit the updated submodule reference:
git add backend
git commit -m "Update backend"
git push
Updating All Submodules
To update all configured submodules:
git submodule update --remote
For nested submodules:
git submodule update --remote --recursive
Be careful with this approach if your master repository is being used as a production release manifest. Updating everything at once may move several components to newer commits.
Pulling Changes from the Master Repository
A common workflow is:
git pull
git submodule update --init --recursive
You can also use:
git pull --recurse-submodules
For teams working heavily with submodules, this is worth understanding because pulling the master repository and updating the submodules are related but separate operations.
Running Commands Across All Submodules
One of my favorite Git submodule commands is:
git submodule foreach git status
Git will execute git status inside every submodule.
You can also run:
git submodule foreach git fetch
Or:
git submodule foreach 'git branch --show-current'
You can even run project commands:
git submodule foreach 'npm install'
This can be very useful when managing a large number of repositories.
Checking Submodule Differences
If a submodule has changed:
git status
may show:
modified: backend (new commits)
To understand what changed:
git diff --submodule
You can also configure Git to display submodule commit history in diffs:
git config --global diff.submodule log
Changing a Submodule Repository URL
If the repository URL changes, use:
git submodule set-url backend https://github.com/example/new-backend.git
Then synchronize:
git submodule sync
For nested repositories:
git submodule sync --recursive
Removing a Submodule
If you no longer need a repository:
git submodule deinit -f backend
Then:
git rm -f backend
Finally:
git commit -m "Remove backend submodule"
git push
Working with Private Repositories
Submodules work with private GitHub repositories as well.
For teams, SSH is often convenient:
git submodule add git@github.com:example/backend.git backend
You can verify your GitHub SSH authentication with:
ssh -T git@github.com
Every developer who clones the master repository needs appropriate access to the private submodule repositories.
This is an important consideration when designing the permissions for your GitHub organization.
Detached HEAD in Submodules
Sometimes you enter a submodule and see:
HEAD detached at 82a31f
This is not necessarily an error.
It happens because the master repository tells the submodule: check out this exact commit.
That is how Git maintains reproducible versions.
If you want to actively develop on a branch:
git switch develop
Then continue working normally.
A Practical Development Workflow
A typical workflow might look like this.
Clone the master repository:
git clone --recurse-submodules https://github.com/example/product-master.git
cd product-master
Check everything:
git submodule status
Go to a project:
cd backend
Switch to your development branch:
git switch develop
Pull the latest changes:
git pull origin develop
Develop and commit:
git add .
git commit -m "Implement payment integration"
git push origin develop
Return to the master repository:
cd ..
Check the master repository:
git status
You will see that the submodule reference changed.
Commit that reference:
git add backend
git commit -m "Update backend submodule"
git push
This gives you two separate Git histories:
Backend Repository
│
└── Payment integration commit
│
▼
Backend commit X
Master Repository
│
└── Points backend to commit X
Production and Release Management
This is where Git submodules become particularly useful.
Imagine a production release:
Product v2.5
Backend → 8a31f2
Frontend → 72bc91
Mobile → 19cd83
Contracts → 91de32
The master repository records these exact versions.
If the backend team releases another version tomorrow:
Backend → abc123
the production master repository doesn't automatically change.
You explicitly update it:
git add backend
git commit -m "Upgrade backend to abc123"
git push
This gives the master repository a role similar to a release manifest.
That can be a very clean architecture for systems where each component is developed and deployed independently.
Git Submodule Cheatsheet
Here is the short version I keep handy.
Add
git submodule add <url> <directory>
Clone with submodules
git clone --recurse-submodules <url>
Initialize existing submodules
git submodule update --init --recursive
List submodules
git submodule
Check status
git submodule status
Enter submodule
cd <directory>
Switch branch
git switch <branch>
Pull branch
git pull origin <branch>
Update one submodule
git submodule update --remote <submodule>
Update all
git submodule update --remote
Update recursively
git submodule update --remote --recursive
Run command in every submodule
git submodule foreach '<command>'
Fetch every submodule
git submodule foreach git fetch
Check differences
git diff --submodule
Synchronize URLs
git submodule sync --recursive
Change URL
git submodule set-url <submodule> <new-url>
Remove
git submodule deinit -f <submodule>
git rm -f <submodule>
Configure branch
[submodule "backend"]
path = backend
url = https://github.com/example/backend.git
branch = develop
Show branches
git -C backend branch -a
Show remote
git -C backend remote -v
Show current branch
git -C backend branch --show-current
Final Thoughts
Git submodules are sometimes considered complicated, but the underlying concept is actually quite simple:
Each project remains an independent repository
+
Master repository tracks exact versions
=
Controlled multi-repository architecture
The biggest thing to remember is that the master repository tracks a commit, not simply a branch.
Branches are useful when you are developing inside a submodule. Commits are what the master repository ultimately records.
For a product consisting of several independently managed repositories, this gives you a useful balance between a monorepo and completely separate repositories:
Master
│
┌──────────────┼──────────────┐
│ │ │
Backend Frontend Mobile
│ │ │
Independent Independent Independent
Repo Repo Repo
│ │ │
└──────────────┼──────────────┘
│
Exact commits
tracked by master
If you establish a clear branch strategy and make the master repository responsible for the versions of each component, Git submodules can work very well for this kind of architecture.

