The DI Trap: Why Senior Engineers Fail When Hilt Disappears

Moving beyond library usage to mastering the system design, object ownership, and lifecycles that FAANG interviews actually test.

The DI Trap: Why Senior Engineers Fail When Hilt Disappears

Last week, I asked a Senior Android Developer a question that led to a very long, uncomfortable silence:

“If Hilt was removed from your project tomorrow, could you build a system to replace it from scratch?”

The silence wasn’t because they were a bad developer. It was because they had mastered a library but neglected the system design behind it. We’ve become experts at adding @Inject and @HiltViewModel, but we often lose sight of the "why."

In high-stakes Staff-level or FAANG interviews, knowing Hilt syntax is secondary. Interviewers want to know if you can design a complex dependency injection system in Android without causing a memory meltdown.

1. A Real-World Failure: The Triple-Session Ghost

In one production app I audited, we discovered a bizarre bug: users were being randomly logged out, even with valid tokens.

The culprit? We found three separate instances of a SessionManager living in memory simultaneously. Because the team didn't understand the Application Dependency Graph, one feature created its own instance, the network layer created another, and the UI created a third. Each had a different "truth" about the user’s state.

This wasn’t a bug — it was a system design failure. This is what happens when you treat DI as “magic” rather than a strict architectural constraint.

2. Why Dependency Injection Exists at Scale

Most developers stop at “testability” or “cleaner code.” While those are vital benefits, they are side effects of a deeper goal.

At scale, the primary reason Dependency Injection (DI) exists is to implement Inversion of Control (IoC) where an external system takes responsibility for providing dependencies, thereby controlling object creation and lifetime. DI is simply the most common way we implement IoC in modern applications. Instead of a class creating its own dependencies, that responsibility is inverted.

Following DI architecture in Android is essential to avoid tight coupling. Look at this common pattern in unmanaged apps:

class UserService {
// Hidden Dependencies: No external control over lifecycle
private val api = ApiClient()
private val db = Database()

fun getUserData() { /* ... */ }
}

The Problem: UserService now "owns" the API and DB. You can't swap them for testing, and you can't ensure that other classes are using these same instances. This is how "Instance Bloat" starts.

This might work in small apps — but at scale, it becomes unpredictable and hard to debug.

3. Dependency Injection System Design: The Comparison

Here’s a simplified comparison of unmanaged vs managed dependency graphs:

Press enter or click to view image in full size
Dependency Injection System Design: The Comparison

4. The Staff-Level Framework: The 3 Questions

To implement Android Dependency Injection best practices, you must move beyond the surface level and answer three fundamental questions for every object:

  1. Who creates the object? (The Injector/Container)
  2. Who owns the object? (Cleanup Responsibility)
  3. How long should it live? (Lifecycle Validity)

The Trade-off: This control comes with a cost — increased initial complexity and the need for strict graph discipline across the team.

5. Principles to Practice: Designing a Manual DI Container

Hilt doesn’t solve DI — it automates a dependency graph. To understand how Hilt works under the hood, you should first be able to design a manual root graph like this:

/**
* The Root Dependency Graph (Application Scope)
* This lives in your Application class and owns long-lived dependencies.
*/

class AppDependencyContainer {
// Controlled creation: These live for the entire app lifetime
val networkClient by lazy { NetworkClient() }
val database by lazy { AppDatabase() }

val userRepository by lazy {
UserRepository(networkClient, database)
}
}

/**
* A Scoped Graph (Feature Level)
* Created when a flow starts, destroyed when it ends to prevent leaks.
*/

class CheckoutFeatureContainer(private val appContainer: AppDependencyContainer) {
val paymentManager by lazy {
PaymentManager(appContainer.networkClient)
}
}

6. Common Interview Mistakes: What NOT to Say

When an interviewer asks “Why do we use DI?”, avoid these traps:

  • ❌ “It makes the code cleaner.” (Too vague. Many systems are “clean” without DI.)
  • ❌ “It’s just for testing.” (Testing is a benefit, but lifetime management is the primary production reason.)
  • ❌ “Hilt handles everything automatically.” (Hilt is just the automation layer; the developer still defines the graph.)

The Senior Answer: “We use DI to implement Inversion of Control, allowing us to centralize object ownership and strictly manage lifecycles across complex feature boundaries.”

🙋 Frequently Asked Questions (FAQs)

What is the biggest mistake when using Hilt?

Treating it as magic and not understanding the underlying dependency graph. This leads to scoping errors and circular dependencies that are difficult to resolve.

When should you NOT use a DI framework?

For extremely small, single-feature apps, a full framework like Hilt might be overkill. However, the principles of Dependency Injection in Android (passing dependencies via constructors) should still be followed to ensure scalability.

Does Hilt use reflection?

No. One of the main reasons Hilt is preferred for Hilt vs Manual DI discussions is that it uses annotation processing (KSP/KAPT) to generate code at compile time, avoiding the runtime overhead of reflection-based object creation.

🔚 Final Thoughts

The next time you type @Inject, remember that Hilt is just a tool. The real work is in the System Design. A DI framework doesn’t remove complexity—it forces you to confront it explicitly.

If your DI setup breaks when Hilt is removed, the problem isn’t Hilt — it’s that the system was never truly designed.

💬 Questions for the viewers:

  • Can you identify one object in your current project that is “scoped” but likely outlives its usefulness?
  • If you had to move a feature to a separate module, how would your DI ownership boundaries change?

Next in Series: In Part 2, we will dive into Visualizing the Application Graph and managing complex feature flows.

📘 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)