Skip to main content
Version: 4.2

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:

TypeLifecycleUse Case
singleSingleton - one instanceShared services, repositories, managers
factoryNew instance each timeControllers, use cases, transient objects
scopedOne instance per scopeActivity/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
info

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 viewModel for 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
info

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:

  1. Type parameter (preferred):

    single<UserRepository> { UserRepositoryImpl() }
  2. Cast operator:

    single { UserRepositoryImpl() as UserRepository }
note

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() DependenciesInjection Parameters
Resolved from KoinProvided at call site
Static dependency graphRuntime values
Must exist in containerPassed 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") }

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
info

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 single for stateful, shared services (repositories, clients, caches)
  • Use factory for stateless, transient objects (controllers, use cases)
  • Use scoped for 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 createdAtStart unless 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