Configuring Git Submodules for Shared Libraries
The same library — a config parser, an internal API client, a set of interface components — lives as copies in several repositories. While the copies diverge slowly, no one notices.
As soon as a fix is released in one of them, versions stop matching: the authorization service already has the new call signature, while billing has the old one.
Then the "whoever is fresher" effect kicks in. The engineer who first pulls the changes gets a build failure on someone else's code, while others continue building on an outdated copy and do not see the problem.
Version desynchronization surfaces on staging or, worse, in production — and people start looking for it not where it originated: first in their own code, then in dependencies, then in the build environment.
Configuring Git Submodules removes the root cause. The shared library remains a separate repository, and the project stores not a copy of its code but a reference to a specific commit.
In the parent repository, it is visible which version of the library is pinned; updating becomes a deliberate action with a separate entry in history, not a side effect of cloning. One library in repositories stops being several different libraries.
Below, we break down what this looks like in practice: what goes into .gitmodules, how to establish an update procedure so that raising the version in one project does not break the build in a neighboring one, and what checks to put into the automated build so that divergence is caught before the branch is merged.
What Changes in the Build When Shared Code Versions Are Under Control
A submodule stores in the parent repository not the "latest version" of the library but a pointer to a specific revision.
Cloning with submodule initialization yields exactly the code that passed acceptance checks — both on the developer's machine and on the build agent. Therefore, a reproducible build stops depending on when and in what order someone synchronized dependencies manually.
Previously, shared code lived as copies: the library was carried into each project, edited in place, and the fix was forgotten to be returned back. Drift accumulated, and seemingly identical builds differed in behavior.
Git with submodules removes the very possibility of such a difference: the parent repository contains a reference, while the body of the library exists only in its own repository.
Pinning the library version turns updating into an explicit action: the pointer changes, the change enters history, and it goes through review like an ordinary edit. Rollback is returning the pointer to the previous revision, without rebuilding the rest of the project.
A single source of code eliminates manual edits of someone else's code: the fix is made in the library repository and reaches all projects when the submodule is updated.
Project build time decreases because manual version synchronization and repeated investigations of divergences disappear. The submodule initialization step on the agent takes seconds, and the result is deterministic: same input — same artifact.
At the same time, a whole class of failures of the "works on my machine" kind goes away — divergence simply has nowhere to come from.
Three Ways to Reuse Shared Code: Comparison of Approaches
The way shared code is reused is primarily a question of who changes the library and how often.
If edits come from one project and are released together with it, there is no point in spreading the code across repositories; if the library is consumed by five teams with different release cadences, a version contract is needed, not a shared folder.
| Option | When it fits | Limitations |
|---|---|---|
| Submodules or a single repository | Multiple repositories, the library lives separately, strict binding of the project to a specific revision is needed | A reference to a commit instead of a branch: updates only by an explicit command; an outdated reference does not break the build but quietly pulls old code |
| Package registry | The library is consumed by different projects, they have their own release cycle and need versions with compatibility ranges | Requires a registry, publishing rules, and semantic versioning; an edit becomes available only after a new version is released |
| Copying the dependency into the tree | Rare updates and a requirement to build without accessing external sources | The same library diverges by versions between projects; local patches are lost on the next update |
Separately about the limitations of submodules that most often surface during the process. The working copy after cloning is empty until recursive traversal is performed — this regularly breaks builds in continuous integration systems if the step is not explicitly specified.
Inside a submodule, the repository is in a detached branch state, and an edit made "in place" goes as a commit into a branch the engineer was not thinking about.
Therefore, the choice of option should be fixed in rules: where changing the reference version is permissible and who reviews that change.
Procedure for Configuring and Checking Submodules in a Project
A submodule pins not a branch but a specific commit of an external repository, so "just adding" it is not enough.
Without a dependency audit and a unified branch scheme, the build is reproducible only for the author of the edits: everyone else gets a different revision of the shared library.
Six Steps: From Inventory to Handover
-
Dependency audit. We inventory shared libraries: what is connected by copying, what as a nested repository, what manually. For each, we record the owner, edit frequency, and number of consumer projects.
-
Branch scheme. We agree on where the revision comes from: release consumers look at the maintenance branch, active development looks at the main one. In the submodule, a tag or commit is pinned, not the tip of a branch.
-
Connection. We specify the path and method of obtaining each submodule: a clean copy of the repository should be deployed with one command, without manual additional installations.
-
Submodule update policy. Updating goes as a separate change indicating the reason, goes through review, and enters the main branch together with code edits. Automatic "pull latest" is excluded.
-
Reproducibility check. In a clean environment, the build runs only from pinned revisions, nothing is pulled outside the declared sources; divergence is visible before release, not after.
-
Pre-release check and handover. Before release, we reconcile revisions with the dependency registry and run a build from a clean copy — divergence blocks the release. We hand over the dependency map, update rules, and rollback procedure.
What We Hand Over to the Team After Implementation
After implementation, the team is left not with a "configured repository" but with a kit with which a new engineer brings up the environment in one pass, and an update of the shared library propagates through projects without manual edits in each.
Below is the composition of this kit and what in it has been verified on real builds rather than described "from memory."
- Submodule configurations — a file with paths, URLs, and branch for each connection: a tag or branch is pinned, partial cloning is enabled, and rules for ignoring local changes are set. The team can see which connection is public and which is private.
- Dependency map — a table "project → library → pinned version → owner." With it, in a minute you can find which projects an edit in the shared library will affect and who needs to be notified.
- Update policy — who moves the version pointer, on what event (library release, vulnerability closure), and what must turn green in checks before merging.
- Access rights — a matrix "role → read/write" for each storage: a contractor is granted read access to the library without the right to change versions in the main project.
- Developer instructions — short scenarios: initial deployment, adding a connection, migrating a project to a new version, typical errors and their symptoms.
- Rollback procedure — how to return the previous version of the library in one project and how to roll back a mass update if checks reveal a regression.
- Build check policy — rules that prevent merging a change with a "floating" version or unresolved conflicts in connections.
The kit lies next to the code and is updated together with it: configurations — in the repository, the map and policy — in the team's shared documentation. Describe your repository scheme — we will come back with an estimate and an implementation plan.
Case: Eliminated Version Desynchronization in Three Teams
Before: Shared Library in Three Copies
Three product teams maintained a shared module — types, an external service client, a logging layer. Each copied it into its own repository and edited it for itself, so by release the versions diverged, and failures already happened in the pipeline.
Every fourth build stopped due to a library version mismatch, and manual reconciliation of edits took up to six hours per week per team: compare three copies, transfer someone else's edit, and avoid breaking local modifications.
What Changed
The library was moved into a separate repository, connected as Git Submodules, and in each product a specific commit was pinned rather than a branch — the build became reproducible.
Updating goes as a separate task: an engineer raises the pointer to the new revision, runs checks, and only then does it go into the main branch. Breaking changes are published in a major branch, and products migrate to it one by one, not all at once.
Metrics After
The share of builds failing due to the library version dropped from a quarter to isolated cases — usually an explicit conflict visible at the check step. Manual reconciliation of edits disappeared: an edit lives in one place and reaches products through a pointer update. Product build time decreased because the shared part is no longer rebuilt inside each repository.
How Is a Submodule Different from a Package, and What to Do When It Grows?
A submodule is a hard reference to a specific commit of someone else's repository, recorded in the project tree.
Git does not decide for you whether versions are compatible: the pointer is updated manually and pinned with a separate commit, and the library's transitive dependencies do not arrive in the tree.
A package is arranged differently — it is a published artifact with a version, and the dependency manager itself assembles a consistent set.
The boundary runs along release cadence and number of consumers.
While the library changes together with the application and there is one consumer, a submodule is cheaper: source code is visible in the tree, edits happen in one environment, and intermediate states do not need versioning.
As soon as a second or third consumer with an independent schedule appears, backward compatibility and version numbering become mandatory — this is the signal that a package is needed.
An overgrown repository gives itself away by build time and merge queues.
A full migration in one step almost always breaks the build: first the library is moved into a separate repository and published as a package, while the submodule is left as a transitional bridge for those who have not migrated yet.
Consumers are migrated one by one, verifying the contract, and only after that are the references and the .gitmodules file removed.
Separately about responsibility boundaries: a submodule gives everyone the same rights to one repository, so teams block each other. A package separates roles — one team releases versions, the rest only connect them.
Before migration, the versioning policy and stable contract are fixed, otherwise a partial migration will stretch out indefinitely.
Technical Doubts: Branches, Rights, Rollbacks, and Updates
A submodule is pinned in the parent repository by a reference to a specific commit of the shared library, not to a branch.
It is precisely this property that determines how the project behaves when switching branches, rolling back, and delimiting access — and it is around this that most doubts arise before agreeing to such a scheme.
The general principle: submodule initialization is an explicit operation (submodule update --init --recursive), not a side effect of cloning.
Until it is performed, library directories remain empty, and the build fails immediately and clearly, rather than "quietly" moving to outdated code.
What happens when switching a branch?
The submodule working tree does not move by itself — after switching the parent project branch, you need to synchronize the references, otherwise you will build the old library with the new code.
We close this with a hook script: synchronization is performed automatically, and divergence between the pinned reference and the actual state is caught by a check in continuous integration before the build.
How to restrict contractors' rights to shared code?
Access to the library repository is granted separately from access to the project and read-only — via a separate key or a limited-action token.
The contractor builds and debugs with the shared library but cannot rewrite it for themselves; changes go through the repository owner and a mandatory check.
How to roll back a library version without rebuilding everything?
The submodule reference is one line in the parent project history. Returning to the previous library commit is done by editing this reference and is pinned as a separate change: the rest of the code is not touched.
We maintain the policy so that rollback takes minutes rather than rebuilding all services, and we verify it with tests before rollout.
Let's Discuss the Task: We'll Analyze the Scheme and Come Back with a Plan
To come back with an estimate and implementation plan, we need not an abstract statement but the actual repository scheme: where shared libraries are located, who connects them now and how, which branches are considered stable.
Based on these inputs, we calculate the scope of work and propose a procedure for migrating to Git Submodules — with rollback points and without stopping current releases.
What is useful to describe on a call or send in advance:
- list of repositories and their owners: applications, services, shared libraries, auxiliary utilities;
- current method of connecting shared modules: code copying, a separate package, nested repositories, synchronization scripts;
- branching scheme and release rules: which branches are long-lived, how edits pass, and how versions are pinned;
- access requirements: which teams need read access to libraries, who is allowed to change the pointer to a specific commit;
- update rules: how often to pull shared library changes into services and who makes the rollback decision;
- set of build checks: what runs when a nested module changes and what blocks merging;
- environment constraints: closed networks, repository mirrors, build specifics on the customer side.
With this set, we fix the repository scheme, break the work into stages, and send an implementation plan with readiness criteria at each step. Describe the task — we will come back with an estimate and a proposal for the order of actions.







