Part 2: Stop Thinking in Classes, Start Thinking in Graphs

 Why the Application Dependency Graph is the secret to Staff-level Android architecture and how to visualize it effectively.

Stop Thinking in Classes, Start Thinking in Graphs

If you want to reach a Staff-level understanding of Android development, you have to stop looking at your project as a folder full of classes.

When a junior developer looks at a feature, they see a list of files: HomeFragment.ktHomeViewModel.ktUserRepository.kt. But when a System Designer looks at that same feature, they aim to design a Directed Acyclic Graph (DAG)—a living map of dependencies flowing from the core of the app out to the user interface.

If you can’t draw your dependency graph, you don’t understand your system.

TL;DR: The Mental Shift

  • Your app is a graph, not a list of classes.
  • DI exists to control object lifecycles, not just for testing.
  • Dependencies must flow downward (UI → Data).
  • Root Graphs are global; Scoped Graphs are temporary.
  • A bad graph design leads to leaks, state bugs, and chaos.

1. The Application Dependency Graph: The Invisible Map

Every Android app has a “hidden” map that dictates how data and logic flow. We call this the Application Dependency Graph. In this graph, every node is an object, and every arrow represents a dependency relationship.

The most dangerous thing you can do as a senior engineer is to let this graph grow organically without a blueprint. Why? Because a “tangled” graph leads to Circular Dependencies — where Class A needs Class B, and Class B needs Class A. In modern DI frameworks like Hilt, this results in a compile-time error; in manual DI, it leads to stack overflows or impossible object creation.

DI frameworks like Hilt don’t create this graph — they enforce the one you design. A poorly designed graph will still be poorly enforced.

2. Mapping the Flow: From UI to Data Layer

A well-architected Android Dependency Injection architecture should only flow in one direction: from the most volatile layer (UI) to the most stable and reusable layer (data layer).

  1. UI Layer (The Consumer): Fragments and Activities. They sit at the edge of the graph and are destroyed frequently.
  2. ViewModel (The Coordinator): Survives configuration changes and connects the UI to business logic.
  3. Repository (The Source of Truth): Orchestrates data from local and remote sources.
  4. DataSources (The Foundation): Stable interfaces like Room DAOs or Retrofit services.

The Golden Rule: Dependencies should only flow downward. Your Repository should never know that a ViewModel exists. If the bottom of your graph depends on the top, your architecture is leaking, making the system fragile and impossible to test.

3. Root Graphs vs. Scoped Graphs

In a robust DI system design, not all objects belong in the same “container.” We categorize them based on their relationship to the app’s process:

These are the “immortals.” They are created when the app starts and die only when the process is killed.

  • Examples: OkHttpClientRoomDatabaseAnalyticsEngine.
  • Design Tip: Only put objects here that are truly global. If you put a “UserLoginToken” in the Root Graph, you’ve created a security risk and a potential state bug when a user logs out and a new one logs in.

These are temporary maps. They are born when a user enters a specific flow and are wiped clean when they leave.

  • Examples: CheckoutRepositoryRegistrationDetailsPaymentProcessor.
  • Design Tip: Use Scoped Graphs to enforce logical isolation. When the user finishes their checkout, the CheckoutFeatureContainer should be destroyed, ensuring sensitive data doesn't linger in memory.

4. Why Lifecycles Dictate Your Graph Design

In Android, Lifecycles are the boundaries of your graph. You should design your DI system based on three primary levels of persistence:

  1. Session Lifecycle: Valid as long as a user is logged in.
  2. Feature/Flow Lifecycle: Valid during a specific task (e.g., a 3-step “Add Property” flow).
  3. Screen Lifecycle: Valid only while a specific Fragment or Activity is active.
/**
* Illustrating how a Scoped Graph "borrows" from the Root Graph.
* The FeatureGraph explicitly depends on its parent to maintain the DAG.
*/

class PaymentFeatureGraph(
private val rootGraph: AppDependencyContainer
) {
// paymentProcessor only exists while this container instance lives.
// It depends on the global NetworkClient from the root.
val paymentProcessor by lazy {
PaymentProcessor(rootGraph.networkClient)
}

val checkoutViewModelFactory by lazy {
CheckoutViewModelFactory(paymentProcessor)
}
}

By explicitly passing the AppDependencyContainer into the PaymentFeatureGraph, you are visually representing that the Feature is a sub-graph of the Application. The Root Graph should never depend on a Scoped Graph, as it would break the direction of the dependency flow.

5. Common Anti-Pattern: The God Singleton

One of the most common mistakes in Hilt component hierarchy design is creating a “God Object” inside the Root Graph that holds references to everything.

  • Why it’s bad: It breaks modularity, creates hidden dependencies, and makes testing nearly impossible because you have to mock the entire world just to test one small feature.
  • The Fix: Split responsibilities into scoped graphs. If a dependency is only used by the “Search” feature, it has no business being in the SingletonComponent.

Common Interview Mistakes

  • Mistake: “I put everything in the SingletonComponent to make it easy to access."
  • The Senior Correction: “Placing every dependency in the SingletonComponent leads to memory bloat and stale state. I prefer defining feature-specific scopes (using components like ActivityRetainedComponent or ViewModelComponent in Hilt) to ensure objects are cleared when their logical scope ends."

🙋 Frequently Asked Questions (FAQs)

What happens if I have a circular dependency?

In Dagger or Hilt, the compiler will catch this and refuse to build. If you are using manual DI, you will likely hit a StackOverflowError at runtime. To fix it, you usually need to extract the shared logic into a new, separate node in the graph.

Does Hilt create these graphs automatically?

Hilt generates the container classes, but you define the structure. Understanding DI best practices in Android means knowing that @InstallIn(SingletonComponent::class) is an architectural choice, not a default setting.

Is there a performance cost to complex graphs?

The graph structure itself isn’t the cost; the cost is object creation. A deep graph where everything is a “Singleton” can slow down app startup as scores of objects are instantiated at once.

✅ DI Graph Health Checklist

  • [ ] Can I draw my dependency graph on a whiteboard right now?
  • [ ] Do all dependencies flow strictly downward (UI → Data)?
  • [ ] Are feature-specific objects properly scoped and cleaned up?
  • [ ] Is there anything in my Root Graph that could actually be scoped?
  • [ ] Have I verified there are no circular dependency “hacks” in the code?

🔚 Final Thoughts

The next time you open your project, try to draw your dependencies. If you find lines crossing everywhere like a bowl of spaghetti, your graph is unhealthy.

Your app isn’t a collection of classes — it’s a graph of lifecycles and dependencies.

Questions for the viewers:

  • Do you have any “Manager” classes in your app that are Singletons but are only actually used in a single feature?
  • Can you trace the dependency path from your Retrofit interface up to your Fragment without any circular jumps?

Next in Series: In Part 3, we will roll up our sleeves and actually Build a Mini-Hilt from Scratch to see how these graphs are constructed in code.

📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.


Comments

Popular posts from this blog

No More _state + state: Simplifying ViewModels with Kotlin 2.3

Why You Should Stop Passing ViewModels Around Your Compose UI Tree 🚫

Is Jetpack Compose Making Your APK Fatter? (And How to Fix It)