One Repo to Rule Them All: Android Modules with Git Submodules

One Repo to Rule Them All: Android Modules with Git Submodules

I was working on a project recently when I noticed something interesting. Another developer had included a springboot kotlin project, built by a different team, into our codebase without copying files, creating a library, or even using Maven or Jitpack. They did it using Git submodules.

At first, I was skeptical. Git submodules have a bit of a reputation: they’re either misunderstood, misused, or completely avoided. But once I took a closer look, I started to see where they could actually shine, especially when working across internal projects or managing open-sourced modules you still want to pull in as source.

This article is a breakdown of how submodules work in the context of Android development, when they make sense, and how to wire them up properly using Gradle. I’m not saying you should start converting all your dependencies into submodules, but if you’re working with internal teams or trying to reuse code without building and publishing artifacts, this might be a clean and simple solution.

Let’s walk through the setup, tradeoffs, and a few things to keep in mind.

Why Submodules? (And When You Shouldn’t Use Them)

Before we dive into how Git submodules work in Android projects, it’s worth pausing to ask: should you even be using them?

Submodules aren’t for every project. If you’re working with public dependencies, fast-moving teams, or anything you can publish to Maven or Jitpack, stick to that. Submodules introduce friction when versioning isn’t tightly controlled, they’re not great for “fire-and-forget” style dependencies.

That said, there are a few situations where submodules can be the perfect middle ground:

  • Internal shared modulesYou have another project inside your company that builds, say, core-ui, analytics-sdk, or network-utils, and you want to use it in multiple apps without copying the code or publishing artifacts.
  • Single open-source modules you maintainLet’s say you open-sourced your image-editor module on GitHub. You don’t want to deal with publishing to Maven, but you still want to include it in your main app. With submodules, you can pull in the module as source and keep it versioned properly.
  • Collaborating across teamsMaybe another team owns a project you want to consume, but you don’t want to fork or duplicate anything. A submodule lets you reference the exact commit you depend on while still keeping both repos clean and separated.

In short, submodules are great when:

  • You own (or trust) the external project
  • You need to modify or inspect the code locally
  • You’re okay with some manual syncing effort
  • You want everything in a single workspace without copying code

They’re not great when:

  • The code changes often and unpredictably
  • You just need binaries
  • You don’t control the other repo

The rest of this guide assumes you’ve decided that submodules make sense for your use case. If you’re here just out of curiosity (like I was), feel free to read along anyway, it’s a useful tool to keep in your toolbox.

Adding a Git Submodule: One Command, Real Impact

Once you’ve decided that a submodule fits your use case, the actual setup is surprisingly simple. From your project’s root directory, run:

git submodule add https://github.com/your-org/anotherProject.git ../anotherProject

Let’s break that down:

  • git submodule add tells Git you’re adding a submodule
  • The first argument is the Git URL of the project you want to include
  • The second argument is the folder path where you want it to live, in this case, one level up (..) so it’s not nested inside your current repo’s directory

After running this, two things will happen:

  • Git will clone the submodule into the path you specified
  • It will create a new file called .gitmodules at the root of your project

That .gitmodules file will look something like this:

[submodule "../anotherProject"]
    path = ../anotherProject
    url = https://github.com/your-org/anotherProject.git

Behind the scenes, Git tracks only a specific commit of the submodule, not the full history like a regular clone. That means:

  • If the submodule gets updated upstream, your project won’t automatically pick it up
  • You control when (and whether) to update to a newer commit

A quick example

Let’s say the submodule repo receives a new commit. In your project, you’ll need to run:

cd ../anotherProject
git pull origin main
cd -
git add ../anotherProject
git commit -m "Update submodule to latest commit"

That’s part of the tradeoff, submodules give you source-level control, but also require manual updates when needed.

Read up more on it here:

Hooking Up Your Submodule in Gradle

Cloning the submodule is just the first step, now you need to tell Gradle where to find it. By default, Gradle only knows about modules inside your project directory. Since we placed the submodule outside (../anotherProject), we’ll need to explicitly register it.

Open your settings.gradle.kts (or settings.gradle if you’re using Groovy) and add the following:

include(":anotherProject")
project(":anotherProject").projectDir = File("../anotherProject")

Let’s break this down:

  • include(":anotherProject") tells Gradle you want to include a new module
  • project(":anotherProject").projectDir = File("../anotherProject") points Gradle to the actual directory of the submodule, relative to the root settings.gradle.kts

You can choose any relative path here, depending on where the submodule lives. Just make sure it resolves correctly from the root project directory.

Once this is in place, you’ll see anotherProject treated like any other module in your IDE. You can browse its code, use its classes, and even run or test it independently.

Gradle doesn’t care whether this folder is part of your Git repo or not, as long as it exists and has a valid build.gradle.kts, it’ll wire it in.

Using the Module in Your App or Feature

Once your submodule is included in settings.gradle, using it is pretty much the same as working with any other local module in an Android project.

Open the build.gradle.kts of your app (or feature module) and add a dependency like this:

implementation(project(":anotherProject"))

Or if you only want it available in debug builds:

debugImplementation(project(":anotherProject"))

Or if you’re exposing its API to downstream modules:

api(project(":anotherProject"))

When to use what

  • implementation most common. You depend on the module, but consumers of your module don't need to see it.
  • apiuse this if your module re-exports types or functions from the submodule.
  • debugImplementation useful for things like logging libraries, dev tools, or mock data generators that you only want during development.

Keep in mind

  • The submodule must have a valid build.gradle.kts (or Groovy version) file.
  • It should ideally follow your project’s conventions, consistent compileSdk, targetSdk, etc.
  • You’ll need to sync Gradle after changes to settings.gradle to make the new module visible.

At this point, you should be able to build and run your app with the submodule wired in. The code is shared, tracked in Git, and sits right there in your IDE like any other module, no artifact publishing, no version mismatches.

Submodule Gotchas & Best Practices

Git submodules can be incredibly useful, but they come with quirks. Here are a few things you’ll want to know before relying on them too heavily.

Cloning a Repo with Submodules

When someone else clones your repo, the submodule’s contents won’t be there automatically. They’ll need to run:

git submodule update --init

Or, better yet, clone with submodules from the start

git clone --recurse-submodules <repo-url>

This is easy to forget, so if you’re onboarding teammates, include this in your project’s README.

Updating Submodules

The main repo tracks a specific commit from the submodule, not the latest code. If you want to update it:

cd ../anotherProject
git pull origin main   # or any branch you want
cd -
git add ../anotherProject
git commit -m "Update submodule"

You’re not just pulling code, you’re updating the reference in your parent repo. That means two commits: one in the submodule, one in the parent.

Removing a Submodule (Cleanly)

Submodules don’t go away by just deleting the folder. To remove one properly:

  • Delete the entry in .gitmodules
  • Delete the section from .git/config
  • Run: git rm --cached ../anotherProject
  • Commit the change
  • Manually delete the submodule directory if needed

Yes, it’s a bit of a cleanup process, another reason to use them only when they’re really adding value.

Best Practices

  • Keep them small and focused, don’t use submodules for huge, unrelated projects
  • Pin to commits intentionally, treat updates like upgrading dependencies
  • Keep READMEs clear, especially around setup instructions
  • Don’t use for public libs, if it’s not internal or owned by you, use Maven/Jitpack

Git submodules are more fragile than Gradle dependencies in some ways, but they also offer more control. If you’re careful, they can make managing internal shared code feel clean and deliberate, without needing to run a Maven repo or deal with versioning headaches.

Real-World Use Case: Open-Sourcing a Single Module

Let’s say you’ve built a reusable image editor as part of your app, maybe something like image-editor, a self-contained module that handles cropping, filters, and transformations.

Now your team decides to open-source it on GitHub. Great! But here’s the catch: you still want to use it internally, in your production app, without publishing it to Maven Central or relying on Jitpack.

This is where submodules really shine.

Here’s how it plays out

  • You create a new public GitHub repo and move the image-editor module there.
  • In your main app repo, you add it as a submodule:
git submodule add https://github.com/your-org/image-editor.git ../image-editor
  • Update your settings.gradle.kts:
include(":image-editor") project(":image-editor").projectDir = File("../image-editor")
  • Use it like any local module:
implementation(project(":image-editor"))

Now you’ve got:

  • An open-sourced module that lives on GitHub
  • An internal app that uses it without copying code or managing a publish pipeline
  • Version control over which commit of image-editor your app depends on

You can work on both projects in the same IDE session, open PRs against the public repo, and keep your internal project lean.

This pattern works great for:

  • SDKs you maintain for partners or external clients
  • Utility modules (core-ui, logging, analytics) that live in their own repo
  • Open source experiments you want to consume internally before releasing fully

Submodules aren’t the only way to do this, you could publish AARs, use Gradle source dependencies, or copy code directly but submodules give you a clean middle ground when you want source-level access with tight Git integration.

Submodules Aren’t Magic, But They’re Useful

Git submodules have been around forever, but in Android development, they’re often overlooked. Maybe because they feel a bit old-school. Maybe because they’ve burned a few people in the past.

But when used intentionally, they can be a solid tool for managing shared code across projects, especially when you’re working within a single org, or trying to reuse an open-source module you maintain without jumping through the Maven publishing hoops.

They’re not a silver bullet. If you’re building something public, fast-moving, or maintained by another team, you’ll probably be better off using Gradle dependencies or setting up an internal artifact repo.

But if you:

  • Own both repos
  • Want to work with source code directly
  • Need clean version control without extra infrastructure

Then Git submodules can fit right into your workflow.

Just remember:

  • Submodules don’t automatically sync, you control the version
  • Setup requires a little boilerplate in settings.gradle
  • Cloning and onboarding needs that extra git submodule update --init step

They’re not for every situation but in the right one, they’re simple, clean, and surprisingly effective.