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 didn’t 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 you’re 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 doesn’t change the underlying compile-time cost.
- With K2 and KSP replacing KAPT, Dagger/Hilt’s 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, isn’t just a performance upgrade — it’s 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 Google’s own benchmarks, KSP delivers 2× faster processing compared to KAPT on equivalent projects. Dagger’s KSP backend is now stable, and Hilt’s KSP migration reached production quality in 2024. This doesn’t 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 haven’t 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 doesn’t use annotation processing at all. Koin’s 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.
The isSorted Functions That Rewired Kotlin 2.4.0 stdlib Logic Before Kotlin 2.4.0, verifying sort order meant either writing a manual loop, abusing zipWithNext(), or mapping to a boolean list — none of which the compiler...
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 isn’t 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
Koin’s runtime resolution model is frequently described as a weakness, but it’s 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. There’s 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 2’s component-and-module pattern forces architectural decisions upfront that teams often don’t anticipate needing until they’re 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 Google’s 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
Hilt’s 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 doesn’t 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 you’re working against Hilt’s 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 Django’s ORM is faster than writing raw SQL until it isn’t.
Uncovering Hidden Kotlin Architectural Pitfalls Kotlin has transformed modern development with its promise of safety, conciseness, and interoperability. However, even in well-intentioned projects, missteps in kotlin architecture can turn expressive features into hidden pitfalls. Features...
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
There’s 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 don’t fit Hilt’s Android-centric model. Hilt is the rational default for standard Android-only apps built on Jetpack — it provides Dagger’s 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. Koin’s 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 don’t map well to Hilt’s 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 don’t 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 doesn’t support cleanly.
Kotlin Bytecode Bloat: What Aggressive Inlining Does to JVM Performance There's a particular kind of performance problem that doesn't show up in unit tests, doesn't trigger alerts, and looks perfectly reasonable in code review. You're...
How does the K2 compiler affect Koin vs Dagger performance?
K2 itself doesn’t directly affect Koin because Koin doesn’t 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 Koin’s 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: