In this part, we’ll explore how Koin dependency injection wires all the components together, creating a cohesive and testable application architecture.
Koin is a lightweight dependency injection framework for Kotlin that:
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
}
}
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()) }
}
val Application.mainModule makes the module available as a propertymodule { } is Koin’s DSL for defining a dependency modulesingle<T> { } creates a singleton (one instance for the entire application)single for resource efficiencyMessageBroker:
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",
)
}
MessageBroker interface binding to RabbitMqMessageBroker implementationthis@mainModule to share the Application’s coroutine scopegetEnv() is a platform-specific function (works on JVM and Native)NotificationService:
single<NotificationService> {
NotificationServiceImpl(
serviceAccountPath = getEnv("SERVICE_ACCOUNT_PATH") ?: "firebase-admin-sdk.json",
)
}
NotificationService interface to NotificationServiceImplfirebase-admin-sdk.json in the project rootSendNotificationHandler:
single { SendNotificationHandler(get()) }
get() automatically resolves and injects NotificationServiceRoutesDependencies:
single { RoutesDependencies(get()) }
get() resolves and injects MessageBrokerFile: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/Koin.kt
fun Application.configureKoin() {
install(Koin) {
modules(mainModule)
}
}
This function:
mainModule containing all dependency definitionsIn 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 delegationget<T>(): Direct retrievalIn Routing Setup (src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/config/Routing.kt):
fun Application.configureRouting() {
routing {
registerRoutes(get())
}
}
get() without type parameter uses type inferenceRoutesDependencies based on the parameter type of registerRoutes()File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/Application.kt
suspend fun Application.module() {
configureKoin()
configureMessageBroker()
configureSerialization()
configureRouting()
}
Order is critical:
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.
Here’s the complete dependency graph:
Application
└── MainModule
├── MessageBroker (RabbitMqMessageBroker)
│ └── coroutineScope (from Application)
├── NotificationService (NotificationServiceImpl)
│ └── serviceAccountPath (from environment)
├── SendNotificationHandler
│ └── NotificationService ← injected
└── RoutesDependencies
└── MessageBroker ← injected
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>()
The Koin integration demonstrates:
In the final part, we’ll explore how to test and demo the complete application.