Compile-Time Safety
The Koin Compiler Plugin validates your dependency graph at compile time — catching missing dependencies, qualifier mismatches, and broken call sites before your app runs.
This replaces runtime verification tools like verify() and checkModules(). If it compiles, it works.
How It Works
The plugin validates your graph at three levels during compilation:
A2 — Per-Module (Early Feedback)
Each module's definitions are checked against visible definitions: its own definitions, explicitly included modules, and @Configuration sibling modules.
@Module(includes = [DataModule::class])
@ComponentScan("app")
class AppModule
// Validates: definitions from AppModule + DataModule
Modules sharing a @Configuration label are mutually visible:
@Module @ComponentScan("core") @Configuration("prod")
class CoreModule // provides Repository
@Module @ComponentScan("service") @Configuration("prod")
class ServiceModule // Service(repo: Repository) → OK, visible from CoreModule
Different labels are isolated:
@Configuration("core")
class CoreModule
@Configuration("service") // different label — CoreModule NOT visible
class ServiceModule // Service(repo: Repository) → ERROR
What A2 catches:
- Missing dependencies
- Qualifier mismatches (
@Named("prod")requested but only@Named("test")provided) - Cross-scope violations
Lazy<T>withoutTprovided- External deps not marked
@Provided
A3 — Full Graph (Complete Guarantee)
At startKoin<T>(), all modules from all sources are assembled and the complete graph is validated. Everything A2 couldn't see — cross-module dependencies, definitions from JARs — is checked here.
@KoinApplication(modules = [CoreModule::class, ServiceModule::class])
object MyApp
startKoin<MyApp> { }
// Validates: ALL definitions from CoreModule + ServiceModule combined
A3 also validates DSL definitions (single<T>(), factory<T>(), etc.) when they are part of the graph.
A4 — Call-Site Validation
Every koinViewModel<T>(), get<T>(), inject<T>() call in your codebase is intercepted. The plugin captures the target type, file, line, and column — then checks that T exists in the assembled graph.
@Composable
fun UserScreen() {
val viewModel: UserViewModel = koinViewModel() // ← A4 validates this
}
class MyFragment : Fragment() {
val service: PaymentService by inject() // ← A4 validates this
}
If UserViewModel isn't in the graph → build error with exact file, line, and column.
Cross-module call sites: If a feature module calls koinViewModel<T>() but doesn't have visibility into the full graph, the plugin generates a call-site hint. When the app module compiles, it discovers these hints from dependency JARs and validates them against the complete graph.
What Gets Validated
| Scenario | Result |
|---|---|
| Non-nullable param, no definition | ERROR |
Nullable param (T?), no definition | OK — uses getOrNull() |
| Param with default value, no definition | OK — uses Kotlin default (when skipDefaultValues=true) |
@InjectedParam, no definition | OK — provided at runtime via parametersOf() |
@Property("key") param | OK — property injection, not DI validation |
List<T> param | OK — getAll() returns empty list if none |
Lazy<T>, no definition for T | ERROR — unwraps to validate inner type |
@Named("x") param, no matching qualifier | ERROR — with hint if unqualified binding exists |
| Scoped dependency from wrong scope | ERROR |
Default value param with @Named qualifier | ERROR — qualifier forces injection |
@Provided type, no definition | OK — externally provided at runtime |
Android framework type (e.g. Context) | OK — hardcoded whitelist |
Safety with Annotations
Annotate your classes, organize them in modules, and the compiler validates everything:
@Singleton
class Database
@Singleton
class UserRepository(private val db: Database)
@KoinViewModel
class UserViewModel(private val repo: UserRepository) : ViewModel()
@Module
@ComponentScan("com.myapp")
class AppModule
The plugin discovers annotated classes via @ComponentScan, validates each module's definitions at A2, and validates the full graph at A3 when you declare your application entry point:
@KoinApplication(modules = [AppModule::class])
object MyApp
startKoin<MyApp> { } // ← triggers A3 full graph validation
Top-level functions are also supported. Annotated top-level functions are discovered by @ComponentScan and validated like class definitions:
@Singleton
fun provideDatabase(): DatabaseService = PostgresDatabase()
@Factory
fun provideCache(db: DatabaseService): CacheService = RedisCache(db)
// ← validated: DatabaseService exists
Use @Configuration labels to organize modules into groups that are validated together:
@Module @ComponentScan("core") @Configuration("prod")
class CoreModule
@Module @ComponentScan("feature") @Configuration("prod")
class FeatureModule // can see CoreModule's definitions
Safety with DSL
The compiler plugin also validates DSL definitions. When you write single<T>(), factory<T>(), or viewModel<T>(), the plugin intercepts the call, auto-wires the constructor, and validates all parameters:
val appModule = module {
single<Database>()
single<UserRepository>() // ← validated: Database exists
viewModel<UserViewModel>() // ← validated: UserRepository exists
}
No manual get() calls needed — the plugin generates them and validates them at the same time.
The create(::T) function is also validated. It calls a function reference (typically a builder function, but can also be a constructor) and validates all its parameters:
fun buildUserRepository(db: Database): UserRepository = UserRepository(db)
val appModule = module {
scope<UserSession> {
scoped { create(::buildUserRepository) } // ← validated: Database exists
}
}
DSL definitions participate in A3 validation (full graph) and A4 validation (call sites). If you use startKoin { modules(appModule) }, the plugin validates all DSL definitions against the assembled graph.
Both Styles Together
You can mix annotations and DSL in the same project. Both are collected into the same validation graph:
// Annotations
@Singleton class Database
// DSL
val featureModule = module {
single<UserRepository>() // ← validated: Database from annotations is visible
}
Error Messages
Errors report the missing type, which definition needs it, and in which module:
[Koin] Missing dependency: Repository
required by: Service (parameter 'repo')
in module: ServiceModule
When a binding exists with a different qualifier, a hint is shown:
[Koin] Missing dependency: NetworkClient (qualifier: @Named("http"))
required by: ApiService (parameter 'client')
in module: AppModule
Hint: Found NetworkClient without qualifier — did you mean to add @Named("http")?
Call-site errors include exact location:
[Koin] Missing definition: com.app.UserRepository
resolved by: koinViewModel<UserViewModel>()
No matching definition found in any declared module.
→ file: UserScreen.kt, line: 12, column: 5
External Types: @Provided
Some types are provided by the platform or by external frameworks at runtime and are never declared as Koin definitions. Mark them with @Provided to skip validation:
@Singleton
class MyViewModel(@Provided val handle: SavedStateHandle)
// No error — SavedStateHandle is marked as externally provided
When to use @Provided:
- Android framework types not in the whitelist — e.g., custom Android services
- Third-party SDK types injected externally — e.g., Firebase, analytics SDKs
- Cross-module types from non-Koin modules — when a dependency comes from a library that doesn't use Koin
- Test doubles — when replacing real implementations in test configurations
// External SDK — not managed by Koin
@Singleton
class AnalyticsService(@Provided val firebaseAnalytics: FirebaseAnalytics)
// Cross-module: provided by another team's module at runtime
@Factory
class PaymentProcessor(@Provided val paymentGateway: PaymentGateway)
Common Android framework types are automatically whitelisted and don't need @Provided:
android.content.Contextandroid.app.Applicationandroid.app.Activityandroidx.fragment.app.Fragmentandroidx.lifecycle.SavedStateHandleandroidx.work.WorkerParameters
Default Values and skipDefaultValues
When skipDefaultValues is enabled (default), parameters with Kotlin default values use the default instead of being resolved from the DI container:
// With skipDefaultValues = true (default):
@Singleton
class ServiceWithDefault(val timeout: Int = 5000)
// → uses Kotlin default (5000), not DI resolution
// Nullable parameters are still injected:
@Singleton
class Service(val dep: Dependency? = null)
// → uses getOrNull() from DI
// Annotated parameters always use DI regardless of defaults:
@Singleton
class Service(@Named("custom") val name: String = "fallback")
// → resolves from DI with @Named("custom") qualifier
// Mixed: some from DI, some from defaults
@Singleton
class ApiClient(
val repo: UserRepository, // → resolved from DI
val timeout: Int = 30_000, // → uses Kotlin default
@Property("api_url") val url: String = "https://api.example.com" // → resolved from DI (annotated)
)
Set skipDefaultValues = false to always inject all parameters from the DI container, ignoring Kotlin default values.
Configuration
Compile-time safety is enabled by default. To disable it:
koinCompiler {
compileSafety = false // Disable compile-time safety checks
}
Other related options:
koinCompiler {
compileSafety = true // Compile-time dependency validation (default: true)
skipDefaultValues = true // Skip injection for params with default values (default: true)
unsafeDslChecks = true // Validate create() is only instruction in lambda (default: true)
}
Migrating from verify() / checkModules()
The compiler plugin replaces runtime verification. You can remove your verification tests:
| Before | After |
|---|---|
module.verify() in test | Compiler plugin (automatic) |
checkModules() in test | Compiler plugin (automatic) |
| Runtime verification | Compile-time verification |
| Manual test setup | No test code needed |
The compiler validates on every build — no test code required.
See Also
- Compiler Plugin Options - All configuration options
- Compiler Plugin Setup - Installation guide
- Starting with Annotations - Getting started
- Playground Apps - Complete reference apps with both annotations (
app-annotations/) and DSL (app-dsl/) approaches