Modules
Koin modules are the building blocks for organizing your dependency injection configuration. This guide covers how to declare, organize, compose, and manage modules effectively.
What is a Module?
A Koin module is a logical container for grouping related definitions. It's declared with the module function and acts as a namespace for your dependency declarations.
val myModule = module {
// Your definitions here
single { DatabaseHelper() }
factory { NetworkClient() }
}
Module Purpose
Modules help you:
- Organize definitions by feature, layer, or domain
- Encapsulate related dependencies together
- Reuse configurations across different contexts (production, testing, etc.)
- Manage visibility and access control in modularized projects
Creating Modules
Basic Module Declaration
val networkModule = module {
single { ApiClient() }
single { TokenManager() }
}
val databaseModule = module {
single { Database() }
single { UserDao(get()) }
}
Module with Eager Creation
Mark a module to create all its singletons immediately at startup:
val coreModule = module(createdAtStart = true) {
single { ConfigurationManager() }
single { LoggingSystem() }
}
Using Multiple Modules
Components don't need to be in the same module. Modules are logical spaces that can reference definitions from other modules. Dependencies are resolved lazily only when requested.
Cross-Module Dependencies
// Data layer
class UserRepository(val database: Database)
class Database
// Presentation layer
class UserViewModel(val repository: UserRepository)
// Separate modules with dependencies
val dataModule = module {
single { Database() }
single { UserRepository(get()) } // Resolves Database
}
val viewModelModule = module {
factory { UserViewModel(get()) } // Resolves UserRepository from dataModule
}
Koin doesn't require explicit imports between modules. Definitions are lazy - they're registered at startup but only instantiated when requested. Dependencies can reference any definition across all loaded modules.
Loading Multiple Modules
Simply list all modules when starting Koin:
startKoin {
modules(
networkModule,
databaseModule,
dataModule,
viewModelModule
)
}
Koin will resolve dependencies across all modules automatically.
Module Override Strategies
Since Koin 3.1.0, the override behavior has been simplified and made more flexible.
Default Override Behavior (3.1.0+)
By default, Koin allows any definition to be overridden. When two definitions have the same mapping (type + qualifier), the last one loaded wins.
val productionModule = module {
single<Service> { ProductionService() }
}
val debugModule = module {
single<Service> { DebugService() }
}
startKoin {
// DebugService will override ProductionService
modules(productionModule, debugModule)
}
Module order matters! The last module in the list takes precedence for conflicting definitions.
Check Koin logs to see when definitions are overridden.
Strict Mode - Disabling Override Globally
To prevent accidental overrides in production, use allowOverride(false):
startKoin {
allowOverride(false) // Strict mode - no overrides allowed
modules(productionModule)
}
With strict mode enabled, Koin throws a DefinitionOverrideException on any override attempt, helping catch configuration errors early.
Explicit Override per Definition (4.2.0+)
When you want strict control (via allowOverride(false)) but still need specific overrides, mark individual definitions with .override():
val productionModule = module {
single<ApiService> { ProductionApiService() }
single<Logger> { ProductionLogger() }
}
val testModule = module {
// Only this definition is allowed to override
single<ApiService> { MockApiService() }.override()
// This would throw an exception without .override()
// single<Logger> { TestLogger() }
}
startKoin {
allowOverride(false) // Strict mode
modules(productionModule, testModule)
}
Use cases for explicit override:
- Testing - Override production services with mocks while keeping strict mode
- Feature Flags - Conditionally override implementations without global override
- Plugin Systems - Allow specific plugins to override defaults
- Environment-specific - Swap implementations for dev/staging/production
Alternative syntax with withOptions:
single<Service> { TestService() } withOptions {
override()
}
Sharing Modules Across Contexts
When declaring modules directly with val, Koin preallocates instance factories. If you need to reuse a module configuration across different contexts (tests, multiple apps, etc.), return the module from a function:
// ❌ Avoid - Preallocates factories in a value
val sharedModule = module {
single { DatabaseHelper() }
factory { NetworkClient() }
}
// ✅ Preferred - Creates fresh module each time
fun sharedModule() = module {
single { DatabaseHelper() }
factory { NetworkClient() }
}
Why use functions?
- Avoids premature factory allocation
- Enables parameterized module creation
- Better for testing and reusability
Parameterized module example:
fun featureModule(enableDebug: Boolean) = module {
single<Logger> {
if (enableDebug) DebugLogger() else ProductionLogger()
}
single { FeatureService(get()) }
}
// Usage
startKoin {
modules(featureModule(enableDebug = BuildConfig.DEBUG))
}
Strategy Pattern with Modules
Since definitions between modules are lazy and resolved at runtime, you can use modules to implement the Strategy pattern - providing different implementations through module selection.
Example: Multiple Datasource Strategies
Consider a repository that needs a datasource, which can be implemented as Local or Remote:
// Domain contracts
class UserRepository(val datasource: Datasource)
interface Datasource {
fun fetchUsers(): List<User>
}
// Implementation strategies
class LocalDatasource : Datasource {
override fun fetchUsers() = /* read from local DB */
}
class RemoteDatasource : Datasource {
override fun fetchUsers() = /* fetch from API */
}
Declare separate modules for each strategy:
// Core module - always included
val repositoryModule = module {
single { UserRepository(get()) } // Will resolve Datasource from loaded module
}
// Strategy modules - choose one
val localDatasourceModule = module {
single<Datasource> { LocalDatasource() }
}
val remoteDatasourceModule = module {
single<Datasource> { RemoteDatasource() }
}
Select strategy at startup:
// Production app - Remote datasource
startKoin {
modules(repositoryModule, remoteDatasourceModule)
}
// Offline mode - Local datasource
startKoin {
modules(repositoryModule, localDatasourceModule)
}
// Test configuration - Mock datasource
val mockDatasourceModule = module {
single<Datasource> { MockDatasource() }
}
startKoin {
modules(repositoryModule, mockDatasourceModule)
}
This pattern is powerful for:
- Environment-specific configurations (dev/staging/production)
- Feature flags (enable/disable features via module selection)
- A/B testing (swap implementations dynamically)
- Testing (replace real services with mocks)
Module Composition with includes() (3.2+)
The includes() function allows you to compose larger modules from smaller, focused modules. This promotes better organization and encapsulation in multi-module projects.
Basic Module Composition
// Small, focused modules
val networkModule = module {
single { ApiClient() }
single { NetworkMonitor() }
}
val storageModule = module {
single { Database() }
single { CacheManager() }
}
// Parent module aggregates related modules
val dataModule = module {
includes(networkModule, storageModule)
// Additional definitions specific to data layer
single { UserRepository(get(), get()) }
}
// In your app module
startKoin {
modules(dataModule) // Automatically loads networkModule and storageModule too
}
When you load dataModule, Koin automatically loads all included modules (networkModule and storageModule). You don't need to list them explicitly.
Use Cases
1. Split Large Modules
Break down a monolithic module into smaller, more maintainable pieces:
// Before - One large module
val appModule = module {
// 50+ definitions here...
}
// After - Organized into focused modules
val authModule = module { /* auth-related definitions */ }
val analyticsModule = module { /* analytics definitions */ }
val storageModule = module { /* storage definitions */ }
val appModule = module {
includes(authModule, analyticsModule, storageModule)
}
2. Control Visibility in Multi-Module Projects
In modularized Android projects, use internal or private to hide implementation details:
// :feature:user module
// Internal - only accessible within this module
private val userDataModule = module {
single { UserDatabaseDao() }
single { UserCache() }
}
// Internal - only accessible within this module
private val userDomainModule = module {
factory { GetUserUseCase(get()) }
}
// Public API - exposed to other modules
val userFeatureModule = module {
includes(userDataModule, userDomainModule)
// Public facing definitions
factory { UserViewModel(get()) }
}
// :app module
startKoin {
modules(userFeatureModule) // Can only access this, not internal modules
}
Nested Includes with Deduplication
Koin automatically flattens module graphs and removes duplicates:
// Shared modules
val loggingModule = module {
single<Logger> { ProductionLogger() }
}
val networkModule = module {
includes(loggingModule)
single { ApiClient(get()) }
}
// Feature modules both depend on shared modules
val userFeatureModule = module {
includes(networkModule, loggingModule)
single { UserRepository(get()) }
}
val productFeatureModule = module {
includes(networkModule, loggingModule)
single { ProductRepository(get()) }
}
// App module
startKoin {
modules(userFeatureModule, productFeatureModule)
}
Result: Koin loads each module exactly once:
loggingModule- loaded once (not 4 times!)networkModule- loaded once (not 2 times!)userFeatureModule- loaded onceproductFeatureModule- loaded once
Module loading is optimized to flatten your module graph and eliminate duplicate module instances automatically.
Real-World Example: Feature Module Architecture
// :feature:checkout module
// Internal data layer
private val checkoutDataModule = module {
single { PaymentApi(get()) }
single { CheckoutDao(get()) }
single { CheckoutRepository(get(), get()) }
}
// Internal domain layer
private val checkoutDomainModule = module {
factory { ProcessPaymentUseCase(get()) }
factory { ValidateCartUseCase(get()) }
}
// Internal presentation layer
private val checkoutPresentationModule = module {
factory { CheckoutViewModel(get(), get()) }
}
// Public API - single entry point
val checkoutFeatureModule = module {
includes(
checkoutDataModule,
checkoutDomainModule,
checkoutPresentationModule
)
}
// :app module
startKoin {
modules(
coreModule,
checkoutFeatureModule, // Clean, single dependency
cartFeatureModule,
profileFeatureModule
)
}
Troubleshooting
If you encounter compilation issues when including modules from the same file:
// Option 1: Use Kotlin's get() operator
val parentModule = module {
includes(childModule1.get(), childModule2.get())
}
// Option 2: Separate modules into different files
// - childModule1.kt
// - childModule2.kt
// - parentModule.kt
See GitHub issue #1341 for details.
Best Practices
Module Organization
Group by feature/layer - Organize modules by domain feature or architectural layer
val networkModule = module { /* network layer */ }
val databaseModule = module { /* database layer */ }
val authModule = module { /* auth feature */ }Use
includes()for composition - Build larger modules from focused, smaller modulesval dataModule = module {
includes(networkModule, databaseModule)
}Keep modules focused - Each module should have a single, clear responsibility
Naming Conventions
- Use descriptive names ending with
Module:networkModule,userFeatureModule - Group related modules:
authDataModule,authDomainModule,authPresentationModule
Module Loading
- Load order matters for overrides - Place override modules last
- Use lazy loading - Leverage Koin's Lazy Modules for on-demand loading
- Minimize eager creation - Use
createdAtStartsparingly to improve startup time
Testing
- Use strategy pattern - Swap implementations via module selection
- Create test modules - Provide mock implementations in test-specific modules
- Use
.override()- Mark test definitions with explicit override in strict mode
Multi-Module Projects
- One public module per feature - Expose a single
featureModuleper Gradle module - Use
private/internal- Hide implementation modules from other features - Centralize shared modules - Place common modules in a
:coremodule
See Also
- Definitions - Learn about
single,factory,scoped - Lazy Modules - Load modules on-demand
- Koin DSL - Complete DSL reference
- Scopes - Scoped definitions