Introduction
In the Android world, Modularization is often advocated as a standard for scalable Android development. We are told to split everything: features, layers, utilities. Ideally, this leads to faster builds, cleaner separation of concerns, and reusable code.
We followed this golden rule. We implemented a strict multi-repository strategy, decoupling components such as: Core, Scanner, UIkit, Tools and Network. It felt like we were doing everything right.
But as our team scaled down, this strategy became a liability. With fewer engineers, the operation overhead of maintaining separate repositories became unsustainable. The breaking point finally arrived when we attempted to upgrade to Kotlin 2.0 and Android SDK 35.
The Breaking Point
This upgrade process exposed the significant technical debt we had accumulated through aggressive modularization.
I spent the migration copy-pasting the same build logic into five different places. Because every build.gradle had its own configurations, a simple -copy-paste wasn’t enough. I had to carefully adjust the Gradle files to ensure compatibility, and make sure they survived the CI build pipeline.
The Decision: Consolidate to Core
To survive these changes and keep future maintenance under control, we made a strategic decision: Consolidate
We decided to prioritize speed over separation. A dedicated repo for a scanner used by only one app was overkill, as was a separate UIKit repo that depended heavily on Core module anyway. By merging these into a single, unified Core module, we significantly reduced maintenance overhead. We haven’t abandoned modularization, we’ve simply right-sized it.
However, we had one strict rule: We could not lose the Git history.
We needed to retain years of git blame data. That historical context is crucial for debugging and understanding the evolution of the codebase. We couldn’t simply copy-paste files into a new repo, doing so would wipe out years of history, erasing the original authors and falsely attributing every line to the person performing the merge.
The Execution: Merging Android Repos Without Losing Git History
The biggest fear when merging repositories isn’t just losing history. It’s false responsibility. If you simply copy-paste files, git blame points to you every single line. Suddenly, you become the ‘author’ of legacy code you’ve never touched.
Here is how I used git merge --allow-unrelated-histories to consolidate fragmented modules into unified Core, without losing a single commit.
Step 1: Prepare the Source Repo
I was dealing with five distinct modules: Core, Scanner, UIKit, Tools and Network. Since the source code for each was isolated in its own package structure, I didn’t have any logic conflicts. The real challenge was at root-level, where I had to resolve conflicts in build.gradle and XML files.
Scanner Repo UIKit Repo├── build.gradle ├── build.gradle <-- CONFLICT└── src/main/res/values/ └── src/main/res/values/ ├── strings.xml ├── strings.xml <-- CONFLICT ├── themes.xml ├── themes.xml <-- CONFLICT └── colors.xml └── colors.xml <-- CONFLICTStep 2: The Unrelated History Merge
First, navigate to your target repository. In my case, I used the Core module as the host for all other modules. Make sure you check out the latest branch (e.g. main) to ensure you are starting from a clean, up-to-date state.
Next, I link other modules to the host. My advice is to add all the remote repositories and fetch their history in advance.
git remote add scanner git@github.com:pixelcarrot/scanner.gitgit remote add uikit git@github.com:pixelcarrot/uikit.gitgit remote add tools git@github.com:pixelcarrot/tools.gitgit remote add network git@github.com:pixelcarrot/network.git
git fetch scannergit fetch uikitgit fetch toolsgit fetch networkOnce everything is fetched, you can proceed merge them one by one, resolving conflicts incrementally.
git merge scanner/main --allow-unrelated-histories# resolve conflicts for the current module before starting the next one
git merge uikit/main --allow-unrelated-historiesgit merge tools/main --allow-unrelated-historiesgit merge network/main --allow-unrelated-historiesStep 3: Clean up and verification
Once the merge is complete, you will see the Network source code integrated into your Core directory. The git history now seamlessly combines commits from other repositories.
If you run git log --graph -oneline, you will see your original Core history interleaved with the history from Scanner, UIKit, etc.
* e4f9a12 (HEAD -> main) Merge remote-tracking branch 'network/main' into main|\| * 72a8b91 (network/main) Fix timeout issue in Retrofit client| * 88c21a0 Add NetworkInterceptor class* | d3b4c12 Merge remote-tracking branch 'tools/main' into main|\ \| * | 99a1b23 (tools/main) Add script for icon generation| * | 44f1c21 Update Gradle script for release builds* | | a1b2c3d Merge remote-tracking branch 'uikit/main' into main|\ \ \| * | | 33d1e4f (uikit/main) Update primary color in themes.xml| * | | 11a2b3c Init UIKit module structure| | | |* | | | 55f6a78 (core) Update dependency versions* | | | 22e1d45 Refactor BaseActivity| | | |* | | | 99z8y7x Initial commit of Core | | | * | | 88x7w6v Initial commit of Scanner | | * | 77v6u5t Initial commit of UIKit | * 66u5t4s Initial commit of ToolsThe Aftermath
With the codebase finally unified, the benefits are clear. I have transitioned from struggling to maintain our architecture to actively optimizing it. I tackled the 16KB page size requirement in one place, rather than chasing configs across five different project.
Conclusion
Architecture isn’t a box you checked, it’s a trade-off you manage. At a larger scale, strict isolation made sense. But for a smaller team, that complexity turned into technical debt. If your architecture is showing you down, merging isn’t a step-backward. It’s an optimization.