In this part, we'll explore how Koin dependency injection wires all the components together, creating a cohesive and
testable application architecture.
What is Koin?
Koin is a lightweight dependency injection framework for Kotlin that:
- Works with Kotlin Multiplatform (JVM, Native, JS, etc.)
- Uses a simple DSL for declaring dependencies
- Provides compile-time safety with Kotlin's type system
- Integrates seamlessly with Ktor
Koin Dependencies
In gradle/libs.versions.toml:
[versions]
koin = "4.1.0"
[libraries]
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
In build.gradle.kts:
sourceSets {
commonMain.dependencies {
implementation(libs.koin.core)
implementation(libs.koin.ktor)
// ... other dependencies
}
}
Main Module Definition
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/di/MainModule.kt
val Application.mainModule
get() = module {
// Services
single<MessageBroker> {
RabbitMqMessageBroker(
coroutineScope = this@mainModule,
host = getEnv("RABBITMQ_HOST") ?: "localhost",
port = getEnv("RABBITMQ_PORT")?.toIntOrNull() ?: 5672,
user = getEnv("RABBITMQ_USER") ?: "guest",
password = getEnv("RABBITMQ_PASSWORD") ?: "guest",
)
}
single<NotificationService> {
NotificationServiceImpl(
serviceAccountPath = getEnv("SERVICE_ACCOUNT_PATH") ?: "firebase-admin-sdk.json",
)
}
// Handlers
single { SendNotificationHandler(get()) }
// Routes
single { RoutesDependencies(get()) }
}
Understanding the Module Structure
-
Extension Property on Application:
-
val Application.mainModulemakes the module available as a property - Provides access to the Application scope within the module
- Enables coroutine scope sharing with the application
-
-
Module DSL:
-
module { }is Koin's DSL for defining a dependency module - All service definitions go inside this block
-
-
Single vs Factory:
-
single<T> { }creates a singleton (one instance for the entire application) - All dependencies here use
singlefor resource efficiency
-
Service Definitions
MessageBroker:
single<MessageBroker> {
RabbitMqMessageBroker(
coroutineScope = this@mainModule,
host = getEnv("RABBITMQ_HOST") ?: "localhost",
port = getEnv("RABBITMQ_PORT")?.toIntOrNull() ?: 5672,
user = getEnv("RABBITMQ_USER") ?: "guest",
password = getEnv("RABBITMQ_PASSWORD") ?: "guest",
)
}
- Defines
MessageBrokerinterface binding toRabbitMqMessageBrokerimplementation - Uses
this@mainModuleto share the Application's coroutine scope - Reads configuration from environment variables with sensible defaults
-
getEnv()is a platform-specific function (works on JVM and Native)
NotificationService:
single<NotificationService> {
NotificationServiceImpl(
serviceAccountPath = getEnv("SERVICE_ACCOUNT_PATH") ?: "firebase-admin-sdk.json",
)
}
- Binds
NotificationServiceinterface toNotificationServiceImpl - Service account path configurable via environment variable
- Defaults to
firebase-admin-sdk.jsonin the project root
Handler Definitions
SendNotificationHandler:
single { SendNotificationHandler(get()) }
- Creates the message handler for processing notification events
-
get()automatically resolves and injectsNotificationService - Koin's type inference determines which dependency to inject
Route Dependencies
RoutesDependencies:
single { RoutesDependencies(get()) }
- Creates the dependencies container for routes
-
get()resolves and injectsMessageBroker - Routes can access dependencies through this container
Koin Configuration
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/Koin.kt
fun Application.configureKoin() {
install(Koin) {
modules(mainModule)
}
}
This function:
- Installs the Koin plugin into the Ktor application
- Registers the
mainModulecontaining all dependency definitions - Must be called before other configuration functions that use dependencies
Using Koin in Configuration
In Message Broker Setup (src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/MessageBroker.kt):
fun Application.configureMessageBroker() = runBlocking {
val messageBroker by inject<MessageBroker>()
messageBroker.initialize()
messageBroker.startConsuming(Constants.RABBITMQ_QUEUE, get<SendNotificationHandler>())
}
Two ways to get dependencies:
-
by inject<T>(): Lazy property delegation -
get<T>(): Direct retrieval
In Routing Setup (src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/Routing.kt):
fun Application.configureRouting() {
routing {
registerRoutes(get())
}
}
-
get()without type parameter uses type inference - Resolves
RoutesDependenciesbased on the parameter type ofregisterRoutes()
Application Bootstrap
File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/Application.kt
suspend fun Application.module() {
configureKoin()
configureMessageBroker()
configureSerialization()
configureRouting()
}
Order is critical:
- configureKoin(): Must be first - sets up the DI container
- configureMessageBroker(): Uses Koin to get MessageBroker and handlers
- configureSerialization(): Sets up JSON handling (no dependencies)
- configureRouting(): Uses Koin to get RoutesDependencies
Environment Configuration
The project uses environment variables for configuration, accessed through getEnv():
| Variable | Default | Purpose |
|---|---|---|
RABBITMQ_HOST |
localhost |
RabbitMQ server hostname |
RABBITMQ_PORT |
5672 |
RabbitMQ server port |
RABBITMQ_USER |
guest |
RabbitMQ username |
RABBITMQ_PASSWORD |
guest |
RabbitMQ password |
SERVICE_ACCOUNT_PATH |
firebase-admin-sdk.json |
Path to Firebase service account JSON |
This approach follows the 12-factor app methodology for configuration management.
Dependency Graph
Here's the complete dependency graph:
Application
└── MainModule
├── MessageBroker (RabbitMqMessageBroker)
│ └── coroutineScope (from Application)
├── NotificationService (NotificationServiceImpl)
│ └── serviceAccountPath (from environment)
├── SendNotificationHandler
│ └── NotificationService ← injected
└── RoutesDependencies
└── MessageBroker ← injected
Benefits of Dependency Injection
-
Testability:
- Easy to mock dependencies in tests
- Can create test modules with mock implementations
- No global state or singletons to reset
-
Modularity:
- Components are loosely coupled
- Can swap implementations without changing consumers
- Clear contracts via interfaces
-
Configuration Management:
- Centralized dependency configuration
- Easy to see all service instantiations
- Simple to add new dependencies
-
Type Safety:
- Compile-time dependency resolution
- IDE support for refactoring
- Clear error messages for missing dependencies
-
Multiplatform Support:
- Koin works across all Kotlin platforms
- Same DI code for JVM and Native
- Platform-specific implementations via expect/actual
Common Patterns
Interface Binding:
single<Interface> { ConcreteImplementation(...) }
Constructor Injection:
single { SomeService(get(), get()) }
Named Dependencies (if needed):
single(named("primary")) { PrimaryService() }
single(named("secondary")) { SecondaryService() }
Lazy Injection:
val service by inject<Service>()
Direct Retrieval:
val service = get<Service>()
Summary
The Koin integration demonstrates:
- Clean dependency injection with minimal boilerplate
- Environment-based configuration with defaults
- Interface-based design for flexibility
- Type-safe dependency resolution
- Multiplatform compatibility
- Centralized service lifecycle management
In the final part, we'll explore how to test and demo the complete application.
Top comments (0)