Koin, Dagger, Hilt: Kotlin Dependency Injection Performance

Your Kotlin dependency injection choice is the difference between a 2-minute build and a coffee break you didnt ask for. For Junior and Middle devs scaling to 80+ modules, the old rules are dead. The Kotlin 2.0 K2 compiler has completely flipped the Koin vs Dagger vs Hilt debate by making KSP the industry standard. Forget outdated tutorials—if youre not accounting for KMP support and compile-time overhead in 2026, your architecture is legacy before it even hits production.


TL;DR: Quick Takeaways

  • Koin resolves dependencies at runtime using a service locator pattern — no annotation processing, faster builds, but errors surface at runtime not compile time.
  • Dagger 2 generates all dependency code at compile time via KAPT, which guarantees type safety but adds significant overhead on large codebases.
  • Hilt is Dagger 2 with Android-specific scaffolding built in — it reduces boilerplate but doesnt change the underlying compile-time cost.
  • With K2 and KSP replacing KAPT, Dagger/Hilts build time penalty shrinks — which changes the calculus for mid-size teams that chose Koin purely for speed.

What Kotlin K2 Actually Changes for DI

The K2 compiler, stable since Kotlin 2.0, isnt just a performance upgrade — its an architectural shift in how the compiler processes code. For dependency injection frameworks, the critical change is the transition from KAPT (Kotlin Annotation Processing Tool) to KSP (Kotlin Symbol Processing). KAPT worked by compiling Kotlin to Java stubs first, then running Java annotation processors on those stubs. That two-step process was the main reason Dagger 2 builds felt sluggish on any project above a few dozen classes.

KSP skips the stub generation entirely and processes Kotlin symbols directly. According to Googles own benchmarks, KSP delivers 2× faster processing compared to KAPT on equivalent projects. Daggers KSP backend is now stable, and Hilts KSP migration reached production quality in 2024. This doesnt make Dagger faster than Koin outright — Koin still wins on build time because it generates zero code — but it significantly narrows the gap that pushed many teams toward Koin in the first place.

K2 Compiler and Annotation Processing: The Real Numbers

The performance story matters most at the extremes. On a small project with 20–30 modules, KAPT overhead is annoying but tolerable — maybe 15–20 extra seconds per clean build. Scale that to a 100-module enterprise Android app and KAPT annotation processing alone can account for 3–5 minutes of build time per full compilation cycle. KSP cuts that by roughly half. For teams stuck on Dagger 2 with KAPT who havent migrated to KSP yet, the migration alone — without changing DI frameworks — can recover more time than switching to Koin would have.

// build.gradle.kts — KSP migration for Dagger 2
plugins {
    id("com.google.devtools.ksp") version "2.0.0-1.0.21"
}

dependencies {
    implementation("com.google.dagger:dagger:2.51")
    ksp("com.google.dagger:dagger-compiler:2.51") // replaces kapt()
}

This single change — swapping kapt() for ksp() in your Dagger setup — is the lowest-effort, highest-return optimization available to any team currently on Dagger 2 with KAPT. The Dagger component graph generation logic stays identical; only the processing mechanism changes. On a mid-size project, expect 40–60% reduction in annotation processing time on incremental builds specifically.

Does K2 Break Existing Koin or Dagger Code?

Koin is largely K2-transparent because it doesnt use annotation processing at all. Koins module DSL is pure Kotlin function calls — single { }, factory { }, viewModel { } — and those compile cleanly under K2 without any changes. Dagger 2 with KAPT technically still works under K2 but runs in compatibility mode, which adds overhead compared to native KSP. Teams running Dagger 2 + KAPT on Kotlin 2.0+ are leaving performance on the table and should treat the KSP migration as mandatory maintenance, not optional cleanup.

Related materials
Kotlin extension functions

Kotlin Extension Functions Pitfalls: The Hidden Cost of "Clean" Syntax Extension functions look like a gift. You take some clunky Java-style utility class, replace it with a clean .doSomething() call, and suddenly the code reads...

[read more →]

Koin vs Dagger 2: The Architectural Argument

The debate between Koin and Dagger 2 is often framed as easy vs powerful — which misrepresents both frameworks. The real difference is where errors get caught and what that costs you. Dagger 2 validates your entire dependency graph at compile time. If you declare a dependency that isnt provided, the build fails. Koin defers that validation to runtime — your app compiles fine, and crashes when the missing dependency is first requested. In a well-tested codebase with good coverage, this distinction matters less than it sounds. In a codebase with 40% test coverage and no DI verification tests, it matters a lot.

Runtime Resolution vs Compile-Time Validation

Koins runtime resolution model is frequently described as a weakness, but its architecturally more nuanced than that. Koin uses a registry pattern: on app start, all modules register their factory functions into a central map keyed by type. When a dependency is requested via get() or by inject(), Koin looks up the factory and executes it. Theres no reflection involved in modern Koin (post-3.x) — the lookup is a typed map operation. The tradeoff is that an incorrectly configured module silently passes compilation and only throws NoBeanDefFoundException at the point of first use.

// Koin module — runtime registration
val networkModule = module {
    single { OkHttpClient.Builder().build() }
    single { Retrofit.Builder().client(get()).build() }
    factory { get().create(ApiService::class.java) }
}

// Missing dependency scenario — compiles fine, crashes on first inject
val repositoryModule = module {
    single { UserRepository(get()) } // get() for ApiService — what if networkModule isn't loaded?
}

The fix Koin provides for this is module verification via the checkModules() API, which can run in a JUnit test and validate the entire dependency graph before production. Any team using Koin without a checkModules() test is running with the safety net removed. This is the operational discipline Dagger enforces by default — but Koin achieves the same outcome with an extra test, not with framework magic.

Dagger 2 Component Architecture at Scale

Dagger 2s component-and-module pattern forces architectural decisions upfront that teams often dont anticipate needing until theyre mid-refactor. Every dependency scope requires an explicit component, and components must be connected through the dependency hierarchy — @Subcomponent for child scopes, component dependencies for cross-module injection. This verbosity is the legitimate criticism of Dagger 2 for juniors: it requires understanding the full dependency graph mentally before writing a single line. The payoff is a codebase where every dependency relationship is explicit, typed, and compiler-verified.

Hilt: Dagger 2 With the Ceremony Removed

Hilt is Googles answer to the most common complaint about Dagger 2 — that setting it up in an Android project requires writing components, subcomponents, and application-level wiring that is identical across every project. Hilt ships with pre-built components for every standard Android lifecycle scope: ApplicationComponent, ActivityComponent, FragmentComponent, ViewModelComponent. You annotate your classes with @HiltViewModel, @AndroidEntryPoint, and @Inject, and Hilt generates the Dagger scaffolding you would have written manually.

Hilt vs Dagger 2: When the Abstraction Costs You

Hilts pre-built component hierarchy is a productivity win on standard Android projects with Jetpack ViewModel, standard Fragments, and Activity-scoped dependencies. It becomes a constraint when your architecture doesnt map cleanly to Android lifecycle scopes. Custom scopes in Hilt require custom components, and custom components in Hilt require more ceremony than in vanilla Dagger 2 — because youre working against Hilts opinionated structure rather than with it. Experienced Android developers building non-standard architectures (modular feature flags, custom lifecycle scopes, Kotlin Multiplatform shared modules) sometimes find raw Dagger 2 more tractable than Hilt for the same reason that Djangos ORM is faster than writing raw SQL until it isnt.

Related materials
Mastering Kotlin Coroutines for...

Kotlin Coroutines in Production I still remember the first time I pushed a coroutine-heavy service to production. On my local machine, it was a masterpiece—fast and non-blocking. But under real high load, it turned into...

[read more →]

Hilt and Kotlin Multiplatform: A Hard Limit

Hilt is Android-only. It uses Android-specific annotations and generates Android-specific code — there is no KMP-compatible version, and Google has not indicated one is planned. Dagger 2 itself is similarly constrained. If your team is on a KMP roadmap and plans to share business logic, ViewModels, or repositories across Android, iOS, and Desktop targets, Koin is currently the only mature DI framework with first-class KMP support. Koin 4.x ships dedicated support for Compose Multiplatform and shared ViewModel injection across platforms. This is not a minor footnote — for any team with KMP ambitions, it makes Koin the only realistic choice regardless of the compile-time tradeoffs.

Criterion Koin 4.x Dagger 2 Hilt
Dependency resolution Runtime (registry lookup) Compile-time (generated code) Compile-time (generated code)
Error detection Runtime (or checkModules() test) Build fails on misconfiguration Build fails on misconfiguration
Build time impact Zero (no code generation) Medium–High (KAPT) / Medium (KSP) Medium–High (KAPT) / Medium (KSP)
KMP support Yes (first-class) No No
Android lifecycle integration Manual or Koin-Android extension Manual Automatic (built-in)
Learning curve (junior dev) Low — DSL is idiomatic Kotlin High — components, scopes, graphs Medium — less Dagger ceremony
K2 / KSP ready Yes (no KAPT dependency) Yes (KSP backend stable) Yes (KSP backend stable)

Which Framework for Which Team

Theres no universally correct answer, but there are patterns in where each framework succeeds and where it creates friction. Koin suits teams that prioritize development velocity, have KMP in their roadmap, or are building apps where the dependency graph is relatively shallow and well-tested. Dagger 2 suits teams with deep expertise, complex custom scopes, or architectural requirements that dont fit Hilts Android-centric model. Hilt is the rational default for standard Android-only apps built on Jetpack — it provides Daggers compile-time safety with 60–70% less boilerplate for common patterns.

The Mid-Size Project Problem

The most contested ground is the 5–25 engineer Android team building a moderately complex app. Koins advocates point to faster onboarding and zero build-time penalty. Dagger/Hilt advocates point to catching wiring errors before they reach QA. Both are right, and the actual decision variable is usually test culture rather than framework capability. A team with thorough integration tests and a checkModules() verification suite gets the same safety guarantees from Koin that Dagger provides structurally. A team that relies on the compiler as a safety net because test coverage is inconsistent should default to Dagger or Hilt — and fix their test coverage separately.

FAQ

Is Kotlin dependency injection with Koin safe for production apps?

Yes, with the appropriate verification in place. Koin is used in production by large applications including apps from companies like SNCF and Decathlon. The critical requirement is implementing checkModules() verification in your test suite, which validates the entire dependency graph before deployment. Without this, missing module registrations surface as runtime crashes rather than build failures. Modern Koin 4.x also provides annotation-based configuration via KSP that adds an additional layer of compile-time validation for teams that want it.

Does Dagger 2 still make sense in 2026, or should teams migrate to Hilt?

Dagger 2 still makes sense for projects with non-standard scoping requirements that dont map well to Hilts component hierarchy. For the majority of Android-only projects built on Jetpack components, migrating to Hilt is a net positive — you get the same compile-time guarantees with significantly less boilerplate. The migration from Dagger 2 to Hilt is well-documented and incremental; you dont need to rewrite all injection at once. The clearest signal that you should stay on raw Dagger 2 is if your architecture requires custom lifecycle scopes that Hilt doesnt support cleanly.

Related materials
Kotlin API Design Pitfalls

Kotlin API Design That Ages Well: What Your Interfaces Won't Tell You Most failures in kotlin api design don't happen at the commit that introduced the problem. They happen three months later, in a module...

[read more →]

How does the K2 compiler affect Koin vs Dagger performance?

K2 itself doesnt directly affect Koin because Koin doesnt use annotation processing. For Dagger and Hilt, K2 combined with KSP migration delivers roughly 2× faster annotation processing compared to the previous KAPT approach. This narrows the build-time gap between Koin and Dagger significantly. On a medium-complexity Android project, the practical difference between Koins zero-generation approach and Dagger with KSP may be under 30 seconds on a clean build — which changes the calculus for teams who chose Koin purely on build speed grounds.

Can you use Koin and Dagger 2 together in the same project?

Technically yes, but it creates genuine operational complexity and is not recommended outside of incremental migration scenarios. The frameworks have different paradigms for scope management — Koin uses DSL-declared scopes while Dagger uses annotated components — and mixing them means your dependency graph exists in two separate systems with no cross-visibility. The practical use case is migrating from one to the other module by module, where coexistence is temporary and bounded. Running both frameworks indefinitely in the same codebase is an architecture smell that signals the migration was abandoned halfway.

Is Koin a dependency injection framework or a service locator?

Koin implements the service locator pattern at its core — classes that use by inject() are requesting dependencies from a global registry rather than receiving them through constructor injection. This is a meaningful architectural distinction because it means Koin-injected classes retain an implicit dependency on the Koin runtime. However, Koin fully supports constructor injection via its module DSL, and when used this way the consuming class has no direct Koin dependency. The service locator vs DI distinction matters most for testability — constructor-injected dependencies can be replaced with mocks without any framework involvement, while field-injected ones require Koin to be initialized even in unit tests.

What is the best dependency injection framework for a new Kotlin Multiplatform project in 2026?

Koin is the only mature DI framework with production-grade KMP support as of 2026. Koin 4.x provides native module definitions that work across Android, iOS, Desktop, and Web targets, with specific support for shared ViewModels in Compose Multiplatform. Dagger 2 and Hilt are Android-only and have no stated plans for KMP compatibility. If your project has any cross-platform ambitions — even long-term — starting with Koin avoids a forced migration later. For pure Android-only projects with no KMP roadmap, Hilt remains a strong default choice.

Written by: