Definitions
Definitions are the building blocks of Koin modules. They describe what to create, how to create it, and when to create it. This guide covers all definition types and their features.
Definition Types Overview
Koin provides three core definition types:
| Type | Lifecycle | Use Case |
|---|---|---|
single | Singleton - one instance | Shared services, repositories, managers |
factory | New instance each time | Controllers, use cases, transient objects |
scoped | One instance per scope | Activity/Fragment-bound objects |
Creating a Module
All definitions live inside modules. Use the module function to create a module:
val myModule = module {
// Your definitions here
single { DatabaseHelper() }
factory { NetworkRequest() }
}
Singleton Definitions
A singleton definition creates a single shared instance that's reused across the entire application.
Basic Singleton
class UserRepository
val myModule = module {
// One instance shared everywhere
single { UserRepository() }
}
Characteristics:
- Instance created on first access (lazy by default)
- Same instance returned for all requests
- Retained until Koin container is closed
- Thread-safe by default
When to Use Singletons
Use single for:
- Repositories and data sources
- Network clients and API services
- Database instances
- Configuration managers
- Caches and session managers
- Any stateful service that should be shared
Factory Definitions
A factory definition creates a new instance every time it's requested.
Basic Factory
class LoginController
val myModule = module {
// New instance each time
factory { LoginController() }
}
Characteristics:
- New instance on each request
- Not retained by Koin container
- Caller responsible for instance lifecycle
- Lightweight and stateless
Koin doesn't retain factory instances. Each call to get<LoginController>() creates a new instance.
When to Use Factories
Use factory for:
- Controllers and presenters
- Use cases (domain layer)
- View models (non-Android - use
viewModelfor Android) - Transient objects
- Stateless services
- Objects with short lifecycles
Scoped Definitions
Scoped definitions are covered in detail in the Scopes documentation.
module {
scope<MyActivity> {
scoped { ActivityPresenter() }
}
}
Definition Lambda Syntax
All definition types (single, factory, scoped) use a lambda to describe how to create the instance.
Lambda Basics
module {
// Lambda expression creates the instance
single { MyService() }
// Result type = component type
// Lambda can be any Kotlin expression
factory {
val config = loadConfig()
ComplexService(config)
}
}
Key points:
- Lambda describes the construction recipe
- Usually calls the constructor, but can be any expression
- Return type determines the definition's type
- Lambda is executed when instance is needed
Dependency Resolution
Use get() within definition lambdas to resolve dependencies from other definitions.
Basic Dependency Injection
class DatabaseHelper
class UserRepository(val database: DatabaseHelper)
class UserViewModel(val repository: UserRepository)
val myModule = module {
single { DatabaseHelper() }
single { UserRepository(get()) } // Resolves DatabaseHelper
factory { UserViewModel(get()) } // Resolves UserRepository
}
How it works:
get()finds and returns the requested dependency- Dependencies resolved at instance creation time
- Constructor injection is the recommended pattern
Koin uses constructor injection - resolve dependencies in class constructors. This ensures instances are created with all required dependencies.
Complex Dependency Graphs
class ApiClient(val tokenManager: TokenManager)
class TokenManager
class UserRepository(val api: ApiClient, val database: Database)
class Database
val myModule = module {
single { TokenManager() }
single { ApiClient(get()) } // Resolves TokenManager
single { Database() }
single { UserRepository(get(), get()) } // Resolves ApiClient and Database
}
Koin automatically resolves the entire dependency graph when you request UserRepository.
Type Binding
Single Type Binding
By default, a definition is bound to the type returned by its lambda:
interface UserRepository
class UserRepositoryImpl : UserRepository
module {
// Bound to concrete type UserRepositoryImpl
single { UserRepositoryImpl() }
// Bound to interface UserRepository (preferred)
single<UserRepository> { UserRepositoryImpl() }
}
Two approaches:
Type parameter (preferred):
single<UserRepository> { UserRepositoryImpl() }Cast operator:
single { UserRepositoryImpl() as UserRepository }
The type parameter approach is preferred and used throughout this documentation.
Multiple Type Binding
Bind a single definition to multiple types using bind:
interface Logger
interface DebugLogger
class ConsoleLogger : Logger, DebugLogger
module {
// Bind to two types
single { ConsoleLogger() } bind Logger::class
// Bind to multiple types
single { ConsoleLogger() } binds arrayOf(
Logger::class,
DebugLogger::class
)
}
Usage:
val logger: Logger = get() // Returns ConsoleLogger instance
val debugLogger: DebugLogger = get() // Returns same ConsoleLogger instance
When to Use Type Binding
- Program to interfaces - Depend on abstractions, not implementations
- Flexibility - Easily swap implementations
- Testing - Replace with mocks/fakes
- Decoupling - Separate interface from implementation
Qualifiers (Named Definitions)
Qualifiers help you distinguish between multiple definitions of the same type.
Basic Qualifiers
module {
single<Database>(named("local")) { LocalDatabase() }
single<Database>(named("remote")) { RemoteDatabase() }
}
// Retrieve by qualifier
val localDb: Database = get(named("local"))
val remoteDb: Database = get(named("remote"))
Qualifier Types
Koin supports three types of qualifiers:
1. String qualifiers:
single<Service>(named("production")) { ProductionService() }
single<Service>(named("test")) { TestService() }
2. Type qualifiers:
single<Service>(named<ProductionService>()) { ProductionService() }
3. Enum qualifiers:
enum class ServiceQualifier { PRODUCTION, TEST }
single<Service>(named(ServiceQualifier.PRODUCTION)) { ProductionService() }
Default Binding Behavior
When no qualifier is specified, Koin uses the type:
module {
single<Service> { ServiceImpl1() } // Default (no qualifier)
single<Service>(named("test")) { ServiceImpl2() } // Named qualifier
}
// Resolves ServiceImpl1
val service: Service = get()
// Resolves ServiceImpl2
val testService: Service = get(named("test"))
When to Use Qualifiers
- Multiple implementations of the same interface
- Environment-specific configurations (dev, staging, prod)
- Feature variants (free vs premium)
- Platform-specific implementations
Injection Parameters
Injection parameters allow you to pass runtime values into definitions.
Basic Usage
class UserPresenter(val userId: String, val view: UserView)
module {
factory { (userId: String, view: UserView) ->
UserPresenter(userId, view)
}
}
// Provide parameters when resolving
val presenter = get<UserPresenter> { parametersOf("user123", myView) }
Parameters vs Dependencies
get() Dependencies | Injection Parameters |
|---|---|
| Resolved from Koin | Provided at call site |
| Static dependency graph | Runtime values |
| Must exist in container | Passed with parametersOf() |
Multiple Parameters
class OrderService(
val orderId: String,
val customerId: String,
val repository: OrderRepository // Resolved from Koin
)
module {
single { OrderRepository() }
factory { (orderId: String, customerId: String) ->
OrderService(orderId, customerId, get())
}
}
// Usage
val service = get<OrderService> { parametersOf("order-123", "customer-456") }
Learn more in the Injection Parameters documentation
Lifecycle Callbacks
onClose Callback
Register cleanup logic to run when a definition is released:
class DatabaseConnection {
fun close() { /* cleanup */ }
}
module {
single { DatabaseConnection() } onClose { connection ->
connection?.close()
}
}
When onClose is called:
- For
single: When Koin container is closed - For
scoped: When scope is closed - For
factory: Never (factories aren't retained)
Common Use Cases
module {
// Database cleanup
single { Database() } onClose { db ->
db?.closeConnections()
}
// Network resource cleanup
single { HttpClient() } onClose { client ->
client?.shutdown()
}
// File handle cleanup
single { FileManager() } onClose { manager ->
manager?.closeAllFiles()
}
}
Eager Instantiation
By default, singletons are created lazily - only when first requested. Use createdAtStart to create instances immediately at startup.
Per-Definition Eager Creation
module {
// Lazy (default) - created on first use
single { UserRepository() }
// Eager - created at startup
single(createdAtStart = true) { ConfigurationManager() }
}
Module-Wide Eager Creation
Mark an entire module for eager creation:
val coreModule = module(createdAtStart = true) {
single { ConfigurationManager() }
single { LoggingSystem() }
single { CrashReporter() }
}
startKoin {
modules(coreModule) // All singletons created immediately
}
When to Use Eager Creation
Use createdAtStart for:
- Configuration - Load settings before app starts
- Crash reporting - Initialize before any errors occur
- Pre-warming - Avoid first-use latency for critical services
- Validation - Fail fast if required services can't be created
For background initialization, use Lazy Modules to load modules asynchronously.
Explicit Override (4.2.0+)
Control which definitions can override others, even in strict mode.
Override in Strict Mode
val productionModule = module {
single<ApiService> { ProductionApiService() }
single<Logger> { ProductionLogger() }
}
val testModule = module {
// Explicitly allowed to override
single<ApiService> { MockApiService() }.override()
// Would throw exception without .override()
// single<Logger> { TestLogger() }
}
startKoin {
allowOverride(false) // Strict mode - no overrides by default
modules(productionModule, testModule)
}
Alternative Syntax
single<Service> { MockService() } withOptions {
override()
}
Use Cases
- Testing - Override production services with mocks
- Feature flags - Conditionally override implementations
- Environment-specific - Dev/staging/production variants
See Modules - Explicit Override for details.
Working with Generics
Koin doesn't distinguish between generic type parameters. List<Int> and List<String> are both seen as List.
The Problem
module {
single { ArrayList<Int>() } // Type: ArrayList
single { ArrayList<String>() } // Also type: ArrayList - CONFLICT!
}
// Error: Koin sees this as attempting to override one definition with another
Solution: Use Qualifiers
Differentiate generic types with qualifiers:
module {
single(named("ints")) { ArrayList<Int>() }
single(named("strings")) { ArrayList<String>() }
}
// Usage
val intList: ArrayList<Int> = get(named("ints"))
val stringList: ArrayList<String> = get(named("strings"))
Alternative: Wrapper Classes
data class IntList(val values: List<Int>)
data class StringList(val values: List<String>)
module {
single { IntList(listOf(1, 2, 3)) }
single { StringList(listOf("a", "b", "c")) }
}
Best Practices
Definition Selection
- Use
singlefor stateful, shared services (repositories, clients, caches) - Use
factoryfor stateless, transient objects (controllers, use cases) - Use
scopedfor lifecycle-bound objects (Activity/Fragment dependencies)
Type Binding
- Prefer interfaces -
single<Repository> { RepositoryImpl() } - Use type parameter - More concise than cast operator
- Bind multiple types - When one class implements multiple interfaces
Qualifiers
- Use sparingly - Multiple unnamed implementations is a code smell
- Prefer module strategy - Use separate modules for variants when possible
- Be consistent - Use string qualifiers for simplicity, enums for type safety
Injection Parameters
- Use for runtime values - User IDs, view references, dynamic configuration
- Don't overuse - If every definition needs parameters, consider restructuring
- Combine with dependencies - Parameters for runtime values,
get()for dependencies
Performance
- Lazy by default - Don't use
createdAtStartunless necessary - Use factories wisely - They create new instances, which has overhead
- Avoid circular dependencies - Leads to runtime errors
Testing
- Use explicit override - Mark test definitions with
.override()in strict mode - Create test modules - Separate module for mocks/fakes
- Module strategy - Swap entire modules for different test scenarios
See Also
- Modules - Organizing and composing modules
- Koin DSL - Complete DSL reference
- Scopes - Scoped definitions and lifecycles
- Injection Parameters - Advanced parameter usage
- Autowire DSL - Constructor-based definitions