In a modular Android app, Clean Architecture can give you clear layers β€” and still leave a practical problem: how do you wire repositories, presenters, and clients in one place without spreading constructors and singletons across the codebase?

That is what a small dependency container is for. This post is about the idea: register once, resolve where you need it, keep lifetimes explicit, and keep the tool small enough to understand. That problem is what led me to ship HADA β€” a lightweight Kotlin container β€” while building modular Android apps.

Why this problem shows up

Almost every screen needs the same kinds of objects: a repository, a use case, maybe a presenter or ViewModel. Passing everything manually through constructors works at first, but it gets noisy fast β€” especially when modules should not know about each other's internals.

A useful container usually needs three things:

Heavy DI frameworks can do all of that. Sometimes you also want something tiny you can read in an afternoon and own completely. That trade-off is the point of a minimal container.

What a minimal container looks like

Think of it as a Kotlin service locator / DI container for the JVM and Android. You create a Container, register factories for the types you need, and resolve them when a class runs.

The API stays small on purpose: register and resolve. No code generation, no annotation processors β€” just Kotlin.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Application / entry point                β”‚
β”‚  Creates Container, registers deps        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Feature modules                          β”‚
β”‚  resolve deps (repositories, presenters…) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Tests                                    β”‚
β”‚  Same API, fake registrations             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Factory vs Singleton

Most dependencies fall into two buckets.

Factory β€” a new instance every time you resolve. Good for lightweight objects or when you want a fresh instance per screen.

Singleton β€” one shared instance for the whole container. Good for repositories, API clients, or anything expensive to create.

Registering a simple chain looks like this:

container register Factory<String> {
    "Message: container demo"
}

container register Singleton<MainRepository> {
    MainRepository(message = container.resolve())
}

Resolving is equally direct:

private val mainRepository: MainRepository = container.resolve()

The container builds the graph lazily: when you resolve MainRepository, it resolves the String dependency first.

Tags when types repeat

Sometimes you need more than one instance of the same type β€” two strings, two API base URLs, two caches. Tags solve that without inventing wrapper classes for everything.

container register Factory<String>(tag = "Title") {
    "Title"
}

container register Factory<String>(tag = "Description") {
    "Description"
}

container register Singleton<MainRepository> {
    MainRepository(
        message = container.resolve(tag = "Title"),
        description = container.resolve(tag = "Description")
    )
}

Tags keep the registry explicit: you always know which string you are asking for.

Wiring it on Android

On Android, the natural home for the container is the Application class. Create it once at startup, expose it to activities and fragments, and keep feature code focused on resolving what they need.

class MyApp : Application() {
    val container: Container = Hada()
}

// In an Activity:
val container = (application as MyApp).container

That pattern scales with modularization: each module can register its own bindings in one bootstrap step, and UI layers stay thin.

What to take away

Today there are excellent options β€” Hilt, Koin, manual constructor injection in Compose. A small custom container was the right trade-off for some of my earlier apps. The lesson I still apply: match the tool to the size of the problem, and keep dependency graphs visible.

Try the demo

If you want to see a concrete open-source version of this idea:

I first wrote about HADA on Medium in 2020; this version is a shorter, architecture-focused update.

I build native iOS and Android apps and share notes like this as I ship. If mobile architecture is your thing too, let's connect on LinkedIn.