The internet will tell you Git submodules are a mistake. That advice assumes you have the luxury of a clean migration. In large enterprise codebases, you often don’t.

There is a pattern worth knowing: the Nx monorepo with Git submodules as a deliberate, time-boxed bridge. It’s not the textbook answer. In the right circumstances, it’s the correct one.

The problem this pattern solves

Imagine a platform that has grown across multiple repositories — a legacy web application, a modern API, a second API handling orchestration. They share a database. Almost every significant feature touches at least two of them.

This is a common trajectory for enterprise systems. What starts as reasonable separation becomes friction at scale: independent branch strategies with no coordination, AI assistants that can’t see across repo boundaries, development tooling that has to be configured separately per repo. The cost isn’t duplicated code. It’s duplicated thinking — context the team has to mentally reload every time they cross a boundary.

The standard prescription is a full monorepo merge: pull all histories into one, unify CI, standardize the branching strategy. It’s the right long-term answer. It’s also the answer that can break working teams overnight.

Why “full merge immediately” fails as a default

If your codebase has hundreds of projects spread across multiple repos, each with its own CI pipeline and its own release cadence, a hard cut to a unified monorepo carries real disruption risk. Developers who are productive today have to change their workflow entirely, immediately. Teams operating under regulated deployment constraints often can’t accept that kind of disruption on a short timeline.

The failure mode to avoid is treating monorepo migration as a binary choice: either you’re fully merged or you’re still siloed. That framing ignores the option of a structured transition with incremental value delivery at each phase.

The bridge pattern

The approach: create an Nx parent monorepo that wires existing repositories together as Git submodules under apps/. The parent provides unified tooling — nx run, nx affected, centralized documentation, cross-project AI context. The app repos remain fully independent. A developer who only needs to work in one of those repos still clones it directly. Nothing in their workflow changes until they choose to adopt the unified layer.

nx affected is particularly valuable in this setup. It analyses the project graph to determine exactly which projects were impacted by a change — including changes that span submodule boundaries — so CI only rebuilds and retests what actually changed. For .NET projects, this requires the @nx/dotnet plugin, which teaches Nx to parse .csproj files and construct the .NET project graph. Without it, Nx cannot reason about dependencies inside your .NET submodules.

This matters because adoption can be opt-in. The monorepo is additive, not mandatory. Teams that want cross-project tooling get it immediately. Teams that don’t aren’t disrupted.

The centralized docs/ layer is where a lot of the value lives. A single authoritative document captures the conventions that all projects share: naming rules, soft-delete patterns, service layer boundaries, ORM version distinctions. Both humans and AI agents load this context at session start. You eliminate the quiet inconsistencies that accumulate when developers are context-switching between repositories that each have their own undocumented norms.

Cross-repo feature coordination

The hardest part of this pattern isn’t the structural wiring — it’s branch coordination. When a feature touches multiple submodule repos, you need branches created consistently across all of them. If you leave this to manual discipline, you’ll get drift: a feature branch exists in two repos but not the third, AI agents are confused about which ticket they’re working on, merge order goes wrong and the parent repo points at stale submodule commits.

The solution is to script the coordination explicitly. An init-feature command takes a ticket ID and creates matching branches in the parent repo and all submodules simultaneously. A lightweight YAML file persists the active feature context so tooling can detect it automatically from the branch name. This removes the “which repo are we in?” confusion without requiring anyone to change how they interact with individual repos.

Merge discipline matters too: submodule repos merge first, then the parent. Document it. Automate the check if you can.

The trade-offs you’re accepting

Submodules introduce real friction. Developers run git submodule update --init --recursive on first clone. The detached HEAD state inside submodules is confusing in many Git clients. Any workflow that bypasses the coordination tooling creates incomplete context.

These are real costs. The question is whether they’re smaller than the cost of the alternative. If the alternative is a migration that disrupts a productive team for weeks or months to satisfy an architectural ideal, the submodule friction may be the better bet — especially if you instrument and document it carefully.

I’ve found that “submodule complexity is documented and tooled around” is a manageable state. “Cognitive load from boundary-crossing is undocumented and growing” is not.

A three-phase structure

If you adopt this pattern, design it as a time-boxed bridge with explicit phases:

  • Phase 1: Submodule wiring, centralized docs, cross-project AI tooling, feature coordination scripts. Delivers immediate value. Validates the approach.
  • Phase 2: Move shared libraries to the monorepo root. Reduce duplication at the code level.
  • Phase 3: Remove submodules entirely. Full integration. This is the textbook answer — arrived at on a timeline the team can actually absorb.

Phase 3 might happen in a year. It might not happen at all, because Phase 1 turned out to be enough. That’s a legitimate outcome. The architecture serves the team, not the other way around.

What to measure

When evaluating whether this pattern is right for your situation, don’t just count duplicated code. Count the number of times developers mentally reload context when working across boundaries. Ask how often AI assistants give advice that makes sense in one codebase but violates conventions in another. Measure how long it takes a new developer to get productive across the full surface area.

Cognitive duplication is harder to quantify than code duplication. It’s just as expensive.

If the textbook answer requires disrupting a working team on a short timeline — question the textbook. The best architectural decision is the one your team can actually adopt.

Further Reading


The views expressed here are my own. Examples and scenarios are composites drawn from broad industry experience and do not represent any specific organization, product, or system.