Lazy Modules and Background Loading
Lazy modules enable asynchronous, parallel module loading to improve startup performance. Instead of loading all modules synchronously at startup, you can defer and parallelize module initialization.
What Are Lazy Modules?
Lazy modules delay module registration and instance creation until explicitly loaded. They're particularly useful for:
- Large applications - Split initialization across multiple threads
- Performance optimization - Reduce startup time
- Conditional features - Load modules only when needed
- Background initialization - Load non-critical modules asynchronously
Defining Lazy Modules
Create lazy modules with the lazyModule function:
// Lazy module - not loaded until explicitly requested
val networkModule = lazyModule {
singleOf(::ApiClient)
singleOf(::NetworkMonitor)
}
val databaseModule = lazyModule {
singleOf(::Database)
singleOf(::UserDao)
}
Composing Lazy Modules
Lazy modules support includes() just like regular modules:
val dataModule = lazyModule {
singleOf(::UserRepository)
}
val featureModule = lazyModule {
includes(dataModule) // Include other lazy modules
singleOf(::FeatureService)
}
Lazy modules won't allocate any resources until loaded via the lazyModules() function.
Loading Lazy Modules
Load lazy modules using lazyModules() within your Koin configuration.
Basic Loading
val analyticsModule = lazyModule {
singleOf(::AnalyticsService)
}
val reportingModule = lazyModule {
singleOf(::CrashReporter)
}
startKoin {
// Load critical modules immediately
modules(coreModule, networkModule)
// Load non-critical modules in background
lazyModules(analyticsModule, reportingModule)
}
Parallel Loading (4.2.0+)
Since version 4.2.0, multiple lazy modules load in parallel, with each module in its own coroutine:
val module1 = lazyModule { singleOf(::DatabaseService) }
val module2 = lazyModule { singleOf(::NetworkService) }
val module3 = lazyModule { singleOf(::AnalyticsService) }
startKoin {
// All three modules load simultaneously!
lazyModules(module1, module2, module3)
}
Performance Impact:
| Scenario | Before 4.2.0 (Sequential) | After 4.2.0 (Parallel) |
|---|---|---|
| 1 module @ 100ms | 100ms | 100ms |
| 3 modules @ 100ms each | 300ms | ~100ms |
| 10 modules @ 100ms each | 1000ms | ~100ms |
Waiting for Completion
All Platforms: waitAllStartJobs()
startKoin {
lazyModules(module1, module2, module3)
}
val koin = KoinPlatform.getKoin()
// Block until all lazy modules are loaded
koin.waitAllStartJobs()
// Now safe to use dependencies from lazy modules
val service = koin.get<AnalyticsService>()
Platform behavior:
- JVM/Native: True blocking with
runBlocking - JS: Uses
GlobalScope.promise(not truly blocking, logs warning)
JVM Only: runOnKoinStarted()
startKoin {
lazyModules(analyticsModule)
}
// JVM-only callback
KoinPlatform.getKoin().runOnKoinStarted { koin ->
// Executes after all lazy modules finish loading
koin.get<AnalyticsService>().trackAppStart()
}
Suspending Alternative: awaitAllStartJobs()
For coroutine contexts or platforms without blocking support:
suspend fun initializeApp() {
startKoin {
lazyModules(module1, module2)
}
// Await without blocking
KoinPlatform.getKoin().awaitAllStartJobs()
// Safe to proceed
println("All modules loaded!")
}
Custom Dispatchers
Control which dispatcher runs lazy module loading:
import kotlinx.coroutines.Dispatchers
startKoin {
// Load on IO dispatcher instead of Default
lazyModules(
databaseModule,
networkModule,
dispatcher = Dispatchers.IO
)
}
Common dispatcher choices:
Dispatchers.Default- CPU-intensive work (default)Dispatchers.IO- I/O operations, file access, networkDispatchers.Main- UI updates (Android/Desktop)
Default dispatcher is Dispatchers.Default if not specified.
Real-World Example
// Core modules - load immediately
val coreModule = module {
single { AppConfig() }
single { UserSession() }
}
// Feature modules - load in background
val analyticsModule = lazyModule {
singleOf(::AnalyticsEngine)
singleOf(::EventTracker)
}
val networkingModule = lazyModule {
singleOf(::ApiClient)
singleOf(::WebSocketManager)
}
val databaseModule = lazyModule {
singleOf(::Database)
singleOf(::UserDao)
}
// Android Application
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MyApp)
// Critical modules load immediately
modules(coreModule)
// Non-critical modules load in parallel in background
lazyModules(
analyticsModule,
networkingModule,
databaseModule,
dispatcher = Dispatchers.IO
)
}
// Optional: Wait for background loading to complete
lifecycleScope.launch {
KoinPlatform.getKoin().awaitAllStartJobs()
Log.d("Koin", "All modules loaded!")
}
}
}
Important Limitations
Avoid Cross-Dependencies
Lazy modules and regular modules should be independent. Don't create dependencies from regular modules to lazy modules:
// ❌ BAD - mainModule depends on lazy module
val lazyAnalytics = lazyModule {
single { AnalyticsService() }
}
val mainModule = module {
single { AppController(get<AnalyticsService>()) } // May fail!
}
startKoin {
modules(mainModule)
lazyModules(lazyAnalytics)
}
// ✅ GOOD - Keep dependencies separate
val lazyAnalytics = lazyModule {
single { AnalyticsService() }
}
val mainModule = module {
single { AppController() }
}
startKoin {
modules(mainModule)
lazyModules(lazyAnalytics)
}
Koin doesn't currently validate dependencies between regular and lazy modules. Ensure regular modules don't depend on lazy module definitions.
Best Practice: Load Order
- Immediate modules - Critical services needed at startup
- Lazy modules - Non-critical, deferrable services
- Wait if needed - Use
waitAllStartJobs()before accessing lazy definitions
When to Use Lazy Modules
Good Use Cases
- Analytics/Tracking - Not needed for core functionality
- Crash Reporting - Can initialize in background
- Feature Modules - Modular features loaded on-demand
- Database/Network - Heavy initialization that can be deferred
- Large Apps - Split startup load across threads
Not Recommended
- Core Services - Critical dependencies needed immediately
- Small Apps - Overhead may exceed benefits
- Tightly Coupled Modules - When modules have many cross-dependencies
API Reference
| Function | Platform | Description |
|---|---|---|
lazyModules() | All | Load lazy modules in background |
waitAllStartJobs() | All | Block until all lazy modules load |
awaitAllStartJobs() | All | Suspend until all lazy modules load |
runOnKoinStarted() | JVM only | Callback after loading completes |
See Also
- Modules - Standard module composition
- Definitions - Eager vs lazy singletons
- Performance - Module loading best practices