Skip to main content
Version: 4.2

Dependency Injection in Ktor

The koin-ktor module provides seamless dependency injection integration for Ktor applications. It offers a dedicated Koin plugin for Ktor that works alongside Ktor's built-in DI system.

Setup

Add the Koin Ktor dependency to your project:

dependencies {
implementation("io.insert-koin:koin-ktor:$koin_version")
implementation("io.insert-koin:koin-logger-slf4j:$koin_version") // Optional logging
}

Installing the Koin Plugin

Install Koin as a Ktor plugin in your Application module:

fun Application.main() {
// Install Koin plugin
install(Koin) {
// SLF4J Koin logger
slf4jLogger()

// Declare modules
modules(appModule)
}
}

Complete Configuration

fun Application.main() {
install(Koin) {
// Logging
slf4jLogger()

// Properties
fileProperties("/application.conf")

// Modules
modules(
networkModule,
repositoryModule,
serviceModule
)

// Create eager instances
createEagerInstances()
}
}

Ktor DI Integration (4.1+)

Koin 4.1+ fully supports Ktor 3.2 and integrates seamlessly with Ktor's built-in dependency injection system.

How It Works

The Koin Ktor plugin automatically sets up integration with Ktor's DI, allowing you to:

  • Inject Ktor dependencies into Koin definitions
  • Use both systems side-by-side
  • Leverage Ktor's ApplicationCall and other framework types

Using Ktor Dependencies in Koin

Define dependencies using Ktor's DI:

fun Application.setupDatabase(config: DbConfig) {
val database = Database(config)

// Provide to Ktor DI
dependencies {
provide<Database> { database }
}
}

Inject Ktor dependencies into Koin definitions:

// Koin can resolve Database from Ktor DI
class CustomerRepositoryImpl(
private val database: Database // Resolved from Ktor DI
) : CustomerRepository {
override fun findAll(): List<Customer> {
return database.query("SELECT * FROM customers")
}
}

fun Application.customerDataModule() {
koinModule {
// Database will be resolved from Ktor DI automatically
singleOf(::CustomerRepositoryImpl) bind CustomerRepository::class
}
}

Architecture Benefits

This integration enables a clean separation:

  • Ktor DI - Framework-level dependencies (Database connections, Configuration)
  • Koin - Application-level dependencies (Repositories, Services, Use Cases)
fun Application.module() {
// Ktor DI - Infrastructure
val config = environment.config
val database = Database(config)
dependencies {
provide<Database> { database }
provide<ApplicationConfig> { config }
}

// Koin - Application logic
install(Koin) {
slf4jLogger()
modules(appModule)
}
}

val appModule = module {
// Koin definitions can use Ktor DI dependencies
singleOf(::CustomerRepository) // Injects Database from Ktor
singleOf(::OrderRepository) // Injects Database from Ktor
singleOf(::CustomerService) // Injects CustomerRepository from Koin
}

Dependency Injection in Ktor

Koin provides extension functions for Ktor's core types, making dependency injection available throughout your application.

Available Injection Points

Koin inject() and get() functions work in:

  • Application
  • Route
  • Routing
  • ApplicationCall (within route handlers)

Application-Level Injection

Inject dependencies at the application level:

fun Application.main() {
// Lazy injection
val helloService by inject<HelloService>()

// Or eager injection
val configService = get<ConfigService>()

routing {
get("/hello") {
call.respondText(helloService.sayHello())
}

get("/config") {
call.respondText(configService.getConfig())
}
}
}

Route-Level Injection

Inject per route or routing block:

fun Route.customerRoutes() {
val customerService by inject<CustomerService>()

get("/customers") {
val customers = customerService.getAllCustomers()
call.respond(customers)
}

get("/customers/{id}") {
val id = call.parameters["id"]?.toInt() ?: return@get call.respond(HttpStatusCode.BadRequest)
val customer = customerService.getCustomer(id)
call.respond(customer)
}
}

Request Handler Injection

Inject directly in route handlers:

routing {
get("/users/{id}") {
val userService = get<UserService>()
val userId = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)

val user = userService.getUser(userId)
call.respond(user)
}
}

Request Scopes (4.1+)

Request scopes create instances that live for the duration of a single HTTP request, perfect for request-specific data and processing.

Declaring Request-Scoped Components

Use requestScope to declare components bound to the request lifecycle:

val appModule = module {
// Singleton - shared across all requests
single { UserRepository() }

// Request scope - new instance per request
requestScope {
scopedOf(::RequestLogger)
scopedOf(::RequestMetrics)
scopedOf(::UserSessionHandler)
}
}

Accessing Request-Scoped Components

Use call.scope.get() to resolve request-scoped dependencies:

routing {
get("/users/{id}") {
val requestLogger = call.scope.get<RequestLogger>()
val metrics = call.scope.get<RequestMetrics>()

metrics.start()
requestLogger.log("Processing user request")

val userId = call.parameters["id"]!!
val userService = get<UserService>()
val user = userService.getUser(userId)

requestLogger.log("Request completed")
metrics.end()

call.respond(user)
}
}

Injecting ApplicationCall

Request-scoped components can automatically inject ApplicationCall:

class RequestLogger(private val call: ApplicationCall) {
fun log(message: String) {
val requestPath = call.request.path()
val method = call.request.httpMethod.value
println("[$method $requestPath] $message")
}
}

class UserSessionHandler(private val call: ApplicationCall) {
fun getUserId(): String? {
return call.request.headers["X-User-ID"]
}

fun isAuthenticated(): Boolean {
return call.request.headers["Authorization"] != null
}
}

Request Scope Lifecycle

requestScope {
scoped { RequestContext(get()) }

// onCreate callback
onCreate { requestContext ->
requestContext.startTime = System.currentTimeMillis()
}

// onClose callback
onClose { requestContext ->
val duration = System.currentTimeMillis() - requestContext.startTime
println("Request completed in ${duration}ms")
}
}
note

Request scopes are created and destroyed for each HTTP request. Instances are not shared between requests, ensuring thread safety and preventing state leakage.

Declaring Modules in Ktor (4.1+)

Koin provides convenient functions to declare modules directly within your Ktor application setup.

Using koinModule

Declare modules inline using Application.koinModule:

fun Application.configureRouting() {
// Declare Koin module specific to this feature
koinModule {
singleOf(::CustomerRepository)
singleOf(::CustomerService)
}

routing {
customerRoutes()
}
}

Using koinModules

Load multiple existing modules:

fun Application.configureCustomerFeature() {
koinModules(
customerRepositoryModule,
customerServiceModule,
customerRoutesModule
)

routing {
customerRoutes()
}
}

Modular Application Structure

Organize your Ktor app by feature:

// Feature 1: Customer Management
fun Application.customerModule() {
koinModule {
singleOf(::CustomerRepository)
singleOf(::CustomerService)
}

routing {
route("/api/customers") {
customerRoutes()
}
}
}

// Feature 2: Order Management
fun Application.orderModule() {
koinModule {
singleOf(::OrderRepository)
singleOf(::OrderService)
}

routing {
route("/api/orders") {
orderRoutes()
}
}
}

// Main application
fun Application.module() {
install(Koin) {
slf4jLogger()
modules(coreModule)
}

// Install feature modules
customerModule()
orderModule()
}

Ktor Events

Monitor Koin lifecycle events within your Ktor application:

Available Events

EventDescription
KoinApplicationStartedKoin container started successfully
KoinApplicationStopPreparingKoin container preparing to stop
KoinApplicationStoppedKoin container stopped

Subscribing to Events

fun Application.main() {
install(Koin) {
slf4jLogger()
modules(appModule)
}

// Listen to Koin lifecycle events
environment.monitor.subscribe(KoinApplicationStarted) {
log.info("Koin started successfully")
// Perform post-startup tasks
get<CacheWarmer>().warmUp()
}

environment.monitor.subscribe(KoinApplicationStopPreparing) {
log.info("Koin stopping - preparing shutdown")
// Prepare for shutdown
get<ConnectionPool>().drain()
}

environment.monitor.subscribe(KoinApplicationStopped) {
log.info("Koin stopped")
// Cleanup complete
}
}

Use Cases for Events

  • Startup: Warm caches, initialize background jobs, verify connections
  • Shutdown: Close connections, flush buffers, save state
  • Monitoring: Track application lifecycle, log metrics

Complete Example

A full Ktor + Koin application:

// Domain
data class User(val id: Int, val name: String, val email: String)

interface UserRepository {
suspend fun findAll(): List<User>
suspend fun findById(id: Int): User?
}

class UserRepositoryImpl(private val database: Database) : UserRepository {
override suspend fun findAll(): List<User> {
return database.query("SELECT * FROM users")
}

override suspend fun findById(id: Int): User? {
return database.queryOne("SELECT * FROM users WHERE id = ?", id)
}
}

class UserService(private val repository: UserRepository) {
suspend fun getAllUsers() = repository.findAll()
suspend fun getUser(id: Int) = repository.findById(id)
}

// Koin Module
val appModule = module {
singleOf(::UserRepositoryImpl) bind UserRepository::class
singleOf(::UserService)

requestScope {
scopedOf(::RequestLogger)
}
}

// Ktor Application
fun Application.module() {
// Setup Database (Ktor DI)
val database = Database(environment.config)
dependencies {
provide<Database> { database }
}

// Install Koin
install(Koin) {
slf4jLogger()
modules(appModule)
}

// Configure routing
routing {
userRoutes()
}
}

fun Route.userRoutes() {
val userService by inject<UserService>()

get("/api/users") {
val logger = call.scope.get<RequestLogger>()
logger.log("Fetching all users")

val users = userService.getAllUsers()
call.respond(users)
}

get("/api/users/{id}") {
val logger = call.scope.get<RequestLogger>()
val id = call.parameters["id"]?.toInt()
?: return@get call.respond(HttpStatusCode.BadRequest)

logger.log("Fetching user $id")

val user = userService.getUser(id)
?: return@get call.respond(HttpStatusCode.NotFound)

call.respond(user)
}
}

Best Practices

Module Organization

  1. Separate infrastructure from application - Use Ktor DI for infrastructure, Koin for business logic
  2. Feature-based modules - Group related services in feature modules
  3. Request scopes for request-specific data - Use request scopes for request context, logging, metrics

Performance

  1. Use singletons for stateless services - Repositories, services, utilities
  2. Request scopes for request data - Avoid storing request state in singletons
  3. Lazy injection when possible - Use by inject() for deferred initialization

Testing

  1. Use Koin test modules - Override production modules with test implementations
  2. Clean up between tests - Stop Koin after each test to reset state
  3. Test routes independently - Mock services and test routing logic separately

See Also