DEV Community

Nathan Fallet
Nathan Fallet

Posted on

Part 5: Wire Everything with Koin - Ktor Native Worker Tutorial

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" }
Enter fullscreen mode Exit fullscreen mode

In build.gradle.kts:

sourceSets {
    commonMain.dependencies {
        implementation(libs.koin.core)
        implementation(libs.koin.ktor)
        // ... other dependencies
    }
}
Enter fullscreen mode Exit fullscreen mode

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()) }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Module Structure

  1. Extension Property on Application:

    • val Application.mainModule makes the module available as a property
    • Provides access to the Application scope within the module
    • Enables coroutine scope sharing with the application
  2. Module DSL:

    • module { } is Koin's DSL for defining a dependency module
    • All service definitions go inside this block
  3. Single vs Factory:

    • single<T> { } creates a singleton (one instance for the entire application)
    • All dependencies here use single for 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",
    )
}
Enter fullscreen mode Exit fullscreen mode
  • Defines MessageBroker interface binding to RabbitMqMessageBroker implementation
  • Uses this@mainModule to 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",
    )
}
Enter fullscreen mode Exit fullscreen mode
  • Binds NotificationService interface to NotificationServiceImpl
  • Service account path configurable via environment variable
  • Defaults to firebase-admin-sdk.json in the project root

Handler Definitions

SendNotificationHandler:

single { SendNotificationHandler(get()) }
Enter fullscreen mode Exit fullscreen mode
  • Creates the message handler for processing notification events
  • get() automatically resolves and injects NotificationService
  • Koin's type inference determines which dependency to inject

Route Dependencies

RoutesDependencies:

single { RoutesDependencies(get()) }
Enter fullscreen mode Exit fullscreen mode
  • Creates the dependencies container for routes
  • get() resolves and injects MessageBroker
  • 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

This function:

  • Installs the Koin plugin into the Ktor application
  • Registers the mainModule containing 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>())
}
Enter fullscreen mode Exit fullscreen mode

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())
    }
}
Enter fullscreen mode Exit fullscreen mode
  • get() without type parameter uses type inference
  • Resolves RoutesDependencies based on the parameter type of registerRoutes()

Application Bootstrap

File: src/commonMain/kotlin/me/nathanfallet/ktornativeworkertutorial/Application.kt

suspend fun Application.module() {
    configureKoin()
    configureMessageBroker()
    configureSerialization()
    configureRouting()
}
Enter fullscreen mode Exit fullscreen mode

Order is critical:

  1. configureKoin(): Must be first - sets up the DI container
  2. configureMessageBroker(): Uses Koin to get MessageBroker and handlers
  3. configureSerialization(): Sets up JSON handling (no dependencies)
  4. 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
Enter fullscreen mode Exit fullscreen mode

Benefits of Dependency Injection

  1. Testability:

    • Easy to mock dependencies in tests
    • Can create test modules with mock implementations
    • No global state or singletons to reset
  2. Modularity:

    • Components are loosely coupled
    • Can swap implementations without changing consumers
    • Clear contracts via interfaces
  3. Configuration Management:

    • Centralized dependency configuration
    • Easy to see all service instantiations
    • Simple to add new dependencies
  4. Type Safety:

    • Compile-time dependency resolution
    • IDE support for refactoring
    • Clear error messages for missing dependencies
  5. 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(...) }
Enter fullscreen mode Exit fullscreen mode

Constructor Injection:

single { SomeService(get(), get()) }
Enter fullscreen mode Exit fullscreen mode

Named Dependencies (if needed):

single(named("primary")) { PrimaryService() }
single(named("secondary")) { SecondaryService() }
Enter fullscreen mode Exit fullscreen mode

Lazy Injection:

val service by inject<Service>()
Enter fullscreen mode Exit fullscreen mode

Direct Retrieval:

val service = get<Service>()
Enter fullscreen mode Exit fullscreen mode

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)