Modularization Is Risk Management, Not Folder Organization
When teams discuss modularization, the conversation often begins with a technical question:
How should we split the codebase?
That question matters, but it is not the most useful place to start.
The more useful question is:
What consequence does this dependency have when an application adopts it?
That is the lens that gradually changed how I approached modularization while building SskCore, an internal Android engineering platform used across multiple applications.
This is not a guide to a particular folder structure, a fixed module count, or a universal architecture. It is a reflection on why dependency boundaries are a form of risk management, and why that matters especially in Android development.
A module is a promise about consequences
It is tempting to think of a module as a folder with a Gradle file. But that description misses its real role.
A module is a boundary. It tells an application, in effect: "If you adopt this capability, these are the responsibilities, trade-offs, and relationships you are choosing."
That makes module design less about rearranging code and more about making consequences visible.
For an application, bringing in a capability can affect much more than the functions it can call. It may influence:
- Build complexity and feedback time
- The dependency graph
- Packaging behavior
- Application size
- Runtime behavior
- Permissions and policy considerations
- Testing surface area
- Release confidence
- Long-term ownership
None of those consequences is automatically bad. The problem begins when they are hidden inside a broad shared dependency and only become visible after adoption.
Good boundaries do not eliminate complexity. They make complexity easier to locate, discuss, and own.
Why one shared library eventually becomes uncomfortable
At the beginning, a single shared module is often a sensible choice. Several applications need common behavior, and centralizing it reduces duplication.
Over time, however, that convenience can create a quiet problem.
One application needs a user-interface concern. Another needs a platform integration. A third needs a data-related utility. The shared dependency grows because adding one more capability is easier than reconsidering the boundary.
Eventually, consumers can inherit things they did not consciously choose.
This is not a failure of shared code. It is a sign that the needs of the consumers have become more varied than the original boundary assumed.
That is the moment to stop asking, "Where can this code live?" and start asking, "Who should have to care that this exists?"
Optionality is a design decision
An application should be able to adopt the infrastructure it needs without inheriting unrelated behavior simply because both pieces of code happened to be shared.
In SskCore, that led to separate, intentional capabilities rather than a single all-encompassing dependency. The aim was not modularity for its own sake. The aim was optionality.
Optionality is valuable because it lets a consumer make a deliberate choice:
- Does this application need this capability now?
- Is its cost appropriate for this application?
- Who owns integration and future change?
- Can it evolve independently from unrelated infrastructure?
Those are product and operational questions as much as they are architecture questions.
For a user-facing application such as BhagavadGita, the result should not feel architectural at all. Users should experience a focused, dependable product. The platform work exists underneath that experience, helping each capability remain intentional as the product evolves.
Android makes dependency consequences more visible
Every ecosystem has dependency management challenges, but Android gives those decisions a particularly wide surface area.
A dependency can influence build behavior, resource processing, packaging, application size, device compatibility, release behavior, and runtime experience. A change that looks small in source code can therefore have effects that only emerge later in integration or release workflows.
That does not mean dependencies should be feared. It means they deserve to be treated as engineering decisions with visible consequences.
The practical shift is simple:
Do not ask only whether a dependency is useful.
Ask what adopting it changes for the application.
That question encourages better conversations before a dependency becomes difficult to undo.
More modules are not automatically better
There is an equal and opposite mistake: treating a high module count as evidence of good architecture.
It is not.
Splitting every small piece of code into an independent module can create fragmentation, extra coordination, and an architecture that is harder to navigate than the original codebase. A boundary that has no meaningful purpose only creates another place for developers to look.
The goal is not maximum separation. The goal is meaningful separation.
A boundary is worth considering when it clarifies one or more of the following:
- Capability ownership
- Consumer choice
- Dependency cost
- Release scope
- Change isolation
- Lifecycle and maintenance responsibility
If a proposed module does not make one of those things clearer, it may not be solving a real problem.
A decision framework for boundaries
There is no universal module map, but a small set of questions can make the decision more deliberate.
1. Is the capability genuinely reusable?
Reuse is not the same as similarity. Two applications can have related needs while still requiring different behavior, product decisions, or ownership. A capability belongs in shared infrastructure when the shared contract is clearer and more durable than copying the code.
2. Can a consumer adopt it intentionally?
A consumer should be able to understand what it is choosing. If adopting a capability silently brings in unrelated responsibilities, the boundary may be too broad.
3. Does it have a coherent lifecycle?
Capabilities change. They need versioning, ownership, documentation, and a migration story. A useful module boundary gives those changes a place to happen without making every consumer absorb every decision.
4. Does it reduce the blast radius of change?
Isolation is not about pretending that systems are independent. It is about making the likely impact of a change easier to reason about before release.
5. Can an unfamiliar engineer understand why it exists?
If a boundary only makes sense to the person who created it, it is not yet doing enough work. Architecture should preserve reasoning, not hide it.
The connection to release confidence
Modularization is often treated as a code-organization concern. In practice, it also shapes release confidence.
When capabilities are intentionally separated, a team can have a clearer conversation about what changed, which consumers are affected, and where verification should be focused. That clarity is valuable even when the final release still involves many moving parts.
The point is not to claim that boundaries make releases risk-free. They do not.
The point is that unintentional coupling creates surprises, and surprises are expensive near release time.
Thinking in terms of dependency consequences helps move that discussion earlier, when decisions are easier to change.
The connection to developer experience
There is also a developer-experience benefit.
Developers move more confidently when the capabilities they use have understandable integration points, clear documentation, and predictable behavior. They spend less time reconstructing hidden assumptions and more time making deliberate changes.
That is why modularization cannot be separated from documentation and tooling. A clean boundary without context still leaves a developer guessing. A well-documented boundary turns structure into a usable contract.
This was one of the lessons that shaped SskCore: platform work is not only about making code shareable. It is about making shared decisions understandable to the next person.
The bigger lesson
Modularization is not an end state. It is an ongoing way of asking whether the shape of a system still reflects the shape of its responsibilities.
The most useful boundaries are not the most numerous ones. They are the ones that make ownership, adoption cost, and change consequences easier to see.
That is why I think of modularization as risk management.
Not because every module makes a system safer, but because intentional boundaries make accidental consequences more visible.
And in a growing Android codebase, making consequences visible is often the first step toward making them manageable.
SskCore is an internal Android engineering platform built from recurring production lessons across multiple applications. This article intentionally discusses principles and outcomes only; it does not describe private implementation details or release mechanisms.
Top comments (0)