Lessons from Modularizing Large Android Apps
What I learned when one Android codebase got too big for one module, and why clear boundaries matter more than fancy architecture words.
Lessons from Modularizing Large Android Apps
I used to think clean architecture was the main answer to a big Android codebase. It helps, but it is not enough if everything still lives in one huge module.
When an app grows, the real pain is not only the code size. It is the hidden coupling. One feature starts touching another feature. A small UI change breaks a shared class. A harmless refactor turns into a long day of fixing imports, DI, and build files.
That is when modularization becomes useful.
Why I started splitting things
In the apps I worked on, the code did not get messy all at once. It got messy in small steps.
One feature grew. Then another one reused some of its classes. Then the shared code became a dumping ground. Then nobody knew which module owned what.
I have seen this in real Android work, including product code that needed to stay stable while still moving fast. If you wait too long, the monolith starts fighting you every time you ship.
What I split first
I do not like big refactors that try to fix everything in one go. That usually means more risk and less delivery.
I start with boundaries that are easy to explain:
:appfor the main shell:core:networkfor Retrofit and OkHttp setup:core:databasefor Room and local storage:core:authfor session and token logic- feature modules for real product areas, not random folders
If a piece of code does not have a clear owner, it will end up in the wrong place. I have seen that happen too many times.
The parts that hurt
Modularization sounds clean on paper. In practice, it creates new problems.
Navigation gets more annoying. Resources need naming rules. Dependency injection gets more strict. Shared classes must move out of the wrong layer.
And yes, it is easy to overdo it. If the app is still small, too many modules can slow you down more than they help.
That is why I do not modularize just to look senior. I modularize when the app starts to punish me for keeping everything together.
The leak lesson
One of the best reasons to keep scope small came from a memory leak I saw in an Android app. A long-lived singleton held a listener, and that listener held a Fragment view.
The app did not crash right away. That was the problem. It just kept the whole view hierarchy in memory.
That kind of bug is hard to spot when everything is tied together. Smaller modules and clearer ownership do not solve every leak, but they make bad references easier to find.
What I would not do again
I would not try to split a legacy app into ten modules in one pass.
I would not create a module just because it sounds neat.
I would not hide shared code in a common bucket and hope it stays clean.
I would not let feature modules depend on each other directly unless there is a very good reason.
If I cannot explain why a module exists in one sentence, it is probably a bad module.
A simple rule
Modularization should answer one question:
Can this part of the app move, change, or fail without dragging the whole codebase with it?
If the answer is yes, the split is probably worth it.
If the answer is no, I keep it simple and ship.
Why this matters for me
This is not just architecture talk. It matters because I am building real products like PTE Flow and HumaWrite, and I want code that can survive growth without turning into a mess.
I also want to keep enough structure so I can move fast without breaking the release every week. That balance is the real job.
So my rule is simple: start small, split only where the pain is real, and keep the boundaries sharp enough that the next person can understand them without a long meeting.
