From 3ff9fa416ac00f1d12adb0c561746983167ca31b Mon Sep 17 00:00:00 2001 From: vnikolova Date: Thu, 22 Jan 2026 17:14:12 +0100 Subject: [PATCH 1/7] add a server-di sample project --- codeSnippets/settings.gradle.kts | 1 + codeSnippets/snippets/server-di/README.md | 12 +++++++ .../snippets/server-di/build.gradle.kts | 29 ++++++++++++++++ .../main/kotlin/com.example/Application.kt | 30 ++++++++++++++++ .../kotlin/com.example/AsyncDependencies.kt | 20 +++++++++++ .../src/main/kotlin/com.example/Database.kt | 5 +++ .../kotlin/com.example/GreetingService.kt | 9 +++++ .../src/main/kotlin/com.example/Logging.kt | 19 +++++++++++ .../kotlin/com.example/OptionalExample.kt | 3 ++ .../kotlin/com.example/PrintStreamProvider.kt | 5 +++ .../kotlin/com.example/QualifiersExample.kt | 14 ++++++++ .../main/kotlin/com.example/Repositories.kt | 9 +++++ .../src/main/resources/application.yaml | 19 +++++++++++ .../server-di/src/test/kotlin/GreetingTest.kt | 34 +++++++++++++++++++ 14 files changed, 209 insertions(+) create mode 100644 codeSnippets/snippets/server-di/README.md create mode 100644 codeSnippets/snippets/server-di/build.gradle.kts create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/Database.kt create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/GreetingService.kt create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/OptionalExample.kt create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/QualifiersExample.kt create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/Repositories.kt create mode 100644 codeSnippets/snippets/server-di/src/main/resources/application.yaml create mode 100644 codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt diff --git a/codeSnippets/settings.gradle.kts b/codeSnippets/settings.gradle.kts index a0a7d3dd8..4d0686edb 100644 --- a/codeSnippets/settings.gradle.kts +++ b/codeSnippets/settings.gradle.kts @@ -163,6 +163,7 @@ module("snippets", "tutorial-server-docker-compose") module("snippets", "htmx-integration") module("snippets", "server-http-request-lifecycle") module("snippets", "openapi-spec-gen") +module("snippets", "server-di") if(!System.getProperty("os.name").startsWith("Windows")) { module("snippets", "embedded-server-native") diff --git a/codeSnippets/snippets/server-di/README.md b/codeSnippets/snippets/server-di/README.md new file mode 100644 index 000000000..007c5f2ed --- /dev/null +++ b/codeSnippets/snippets/server-di/README.md @@ -0,0 +1,12 @@ +# Ktor Dependency Injection sample + +This project demonstrates the usage of Ktor’s built-in Dependency Injection (DI) plugin. +> This sample is a part of the [codeSnippets](../../README.md) Gradle project. + +## Running the Project + +```bash +./gradlew :server-di:run +``` + +Then, navigate to [http://localhost:8080/greet/world](http://localhost:8080/greet/world). \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/build.gradle.kts b/codeSnippets/snippets/server-di/build.gradle.kts new file mode 100644 index 000000000..465ebb488 --- /dev/null +++ b/codeSnippets/snippets/server-di/build.gradle.kts @@ -0,0 +1,29 @@ +val ktor_version: String by project +val kotlin_version: String by project +val logback_version: String by project + +plugins { + application + kotlin("jvm") +} + +application { + mainClass.set("io.ktor.server.netty.EngineMain") +} + +repositories { + mavenCentral() + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } +} + +dependencies { + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") + implementation("io.ktor:ktor-server-di:$ktor_version") + implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") + implementation("io.ktor:ktor-server-config-yaml:${ktor_version}") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") + implementation("ch.qos.logback:logback-classic:${logback_version}") + testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version") + testImplementation(kotlin("test")) +} diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt new file mode 100644 index 000000000..f72f042cf --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt @@ -0,0 +1,30 @@ +package com.example + +import io.ktor.server.application.Application +import io.ktor.server.plugins.di.dependencies +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.routing + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +fun Application.module( + greetingService: GreetingService, + userRepository: UserRepository, +) { + routing { + get("/greet/{name}") { + val name = call.parameters["name"] ?: "World" + call.respondText(greetingService.greet(name)) + } + + get("/db") { + call.respondText("DB = ${userRepository.db}") + } + + get("/optional") { + val optional: OptionalConfig? by dependencies + call.respondText("Optional = $optional") + } + } +} diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt new file mode 100644 index 000000000..d2fc3cfb7 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt @@ -0,0 +1,20 @@ +package com.example + +import io.ktor.server.application.Application +import io.ktor.server.application.log +import io.ktor.server.plugins.di.dependencies +import kotlinx.coroutines.delay + +data class EventsConnection(val connected: Boolean) + +suspend fun Application.installEvents() { + val conn: EventsConnection = dependencies.resolve() + log.info("Events connection ready: $conn") +} + +suspend fun Application.loadEventsConnection() { + dependencies.provide { + delay(200) // simulate async work + EventsConnection(true) + } +} \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Database.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Database.kt new file mode 100644 index 000000000..7c3507077 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Database.kt @@ -0,0 +1,5 @@ +package com.example + +interface Database + +class PostgresDatabase(val url: String) : Database \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/GreetingService.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/GreetingService.kt new file mode 100644 index 000000000..c1d27490b --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/GreetingService.kt @@ -0,0 +1,9 @@ +package com.example + +interface GreetingService { + fun greet(name: String): String +} + +class GreetingServiceImpl : GreetingService { + override fun greet(name: String): String = "Hello, $name!" +} \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt new file mode 100644 index 000000000..c4784d97f --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt @@ -0,0 +1,19 @@ +package com.example + +import io.ktor.server.application.* +import io.ktor.server.plugins.di.dependencies +import java.io.PrintStream + + class Logger(private val out: PrintStream) { + fun log(message: String) { + out.println("[LOG] $message") + } +} + +fun Application.logging(printStreamProvider: () -> PrintStream) { + dependencies { + provide { + Logger(printStreamProvider()) + } + } +} diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/OptionalExample.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/OptionalExample.kt new file mode 100644 index 000000000..163e64b61 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/OptionalExample.kt @@ -0,0 +1,3 @@ +package com.example + +data class OptionalConfig(val value: String) diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt new file mode 100644 index 000000000..796a0d414 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt @@ -0,0 +1,5 @@ +package com.example + +import java.io.PrintStream + +fun stdout(): () -> PrintStream = { System.out } \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/QualifiersExample.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/QualifiersExample.kt new file mode 100644 index 000000000..936533f0a --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/QualifiersExample.kt @@ -0,0 +1,14 @@ +package com.example + +import io.ktor.server.application.* +import io.ktor.server.plugins.di.annotations.Named + +interface PaymentProcessor +class MongoPaymentProcessor : PaymentProcessor +class SqlPaymentProcessor : PaymentProcessor + +fun Application.paymentModule( + @Named("mongo") mongo: PaymentProcessor +) { + log.info("Using payment processor: $mongo") +} diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Repositories.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Repositories.kt new file mode 100644 index 000000000..1718ffb87 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Repositories.kt @@ -0,0 +1,9 @@ +package com.example + +import io.ktor.server.plugins.di.annotations.Property + +fun provideDatabase( + @Property("database.connectionUrl") connectionUrl: String +): Database = PostgresDatabase(connectionUrl) + +open class UserRepository(val db: Database) diff --git a/codeSnippets/snippets/server-di/src/main/resources/application.yaml b/codeSnippets/snippets/server-di/src/main/resources/application.yaml new file mode 100644 index 000000000..1d96e4ca1 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/resources/application.yaml @@ -0,0 +1,19 @@ +ktor: + deployment: + port: 8080 + application: + dependencies: + - com.example.RepositoriesKt.provideDatabase + - com.example.UserRepository + - com.example.GreetingServiceImpl + - com.example.PrintStreamProviderKt.stdout + modules: + - com.example.ApplicationKt.module + - com.example.LoggingKt.logging +database: + connectionUrl: postgres://localhost:3037/admin + +connection: + domain: api.example.com + path: /v1 + protocol: https \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt b/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt new file mode 100644 index 000000000..682f74873 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt @@ -0,0 +1,34 @@ +package com.example + +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.server.plugins.di.dependencies +import io.ktor.server.testing.* +import kotlin.test.* + +class GreetingTest { + @Test + fun testGreeting() = testApplication { + application { + // Override dependencies + dependencies.provide { FakeGreetingService() } + dependencies.provide { FakeUserRepository() } + + module( + greetingService = dependencies.resolve(), + userRepository = dependencies.resolve(), + ) + } + + val response = client.get("/greet/Test") + assertEquals("Fake greeting", response.bodyAsText()) + } +} + +class FakeGreetingService : GreetingService { + override fun greet(name: String) = "Fake greeting" +} + +class FakeUserRepository : UserRepository(FakeDatabase()) + +class FakeDatabase : Database \ No newline at end of file From 455df09ddfdc41993e513a0f0596b6e745604b4f Mon Sep 17 00:00:00 2001 From: vnikolova Date: Wed, 4 Feb 2026 16:47:29 +0100 Subject: [PATCH 2/7] fix test and optional dependencies --- .../server-di/src/main/kotlin/com.example/Application.kt | 3 ++- .../snippets/server-di/src/test/kotlin/GreetingTest.kt | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt index f72f042cf..e03f477b7 100644 --- a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Application.kt @@ -13,6 +13,8 @@ fun Application.module( userRepository: UserRepository, ) { routing { + val optional: OptionalConfig? by dependencies + get("/greet/{name}") { val name = call.parameters["name"] ?: "World" call.respondText(greetingService.greet(name)) @@ -23,7 +25,6 @@ fun Application.module( } get("/optional") { - val optional: OptionalConfig? by dependencies call.respondText("Optional = $optional") } } diff --git a/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt b/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt index 682f74873..cfde054b8 100644 --- a/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt +++ b/codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt @@ -2,7 +2,6 @@ package com.example import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText -import io.ktor.server.plugins.di.dependencies import io.ktor.server.testing.* import kotlin.test.* @@ -10,13 +9,9 @@ class GreetingTest { @Test fun testGreeting() = testApplication { application { - // Override dependencies - dependencies.provide { FakeGreetingService() } - dependencies.provide { FakeUserRepository() } - module( - greetingService = dependencies.resolve(), - userRepository = dependencies.resolve(), + greetingService = FakeGreetingService(), + userRepository = FakeUserRepository(), ) } From 5e2ceb9b6c4cadf305a27d7d3abc9077ca66f319 Mon Sep 17 00:00:00 2001 From: vnikolova Date: Wed, 4 Feb 2026 16:55:49 +0100 Subject: [PATCH 3/7] update the DI intro --- topics/server-dependency-injection.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/topics/server-dependency-injection.md b/topics/server-dependency-injection.md index 32f799065..52678ed20 100644 --- a/topics/server-dependency-injection.md +++ b/topics/server-dependency-injection.md @@ -8,12 +8,21 @@

Required dependencies: io.ktor:ktor-server-di

+ + -The Dependency Injection (DI) plugin allows you to register services and configuration objects once and inject them -into your application modules, plugins, routes, and other components throughout your project. Ktor's DI is designed to -integrate naturally with its existing application lifecycle, supporting scoping and structured configuration -out of the box. +[Dependency injection (DI)](https://en.wikipedia.org/wiki/Dependency_injection) is a design pattern that helps you +supply components with the dependencies they require. Instead of creating concrete implementations directly, modules +depend on abstractions, and a DI container is responsible for constructing and providing the appropriate instances at +runtime. This separation reduces coupling, improves testability, and makes it easier to replace or reconfigure +implementations without modifying existing code. + +Ktor provides a built‑in DI plugin that lets you register services and configuration objects once and access them +throughout your application. You can inject these dependencies into modules, plugins, routes, and other Ktor components +in a consistent, type‑safe way. The plugin integrates with the Ktor application lifecycle and supports scoping, +structured configuration, and automatic resource management, making it easier to organize and maintain application‑level +services. ## Add dependencies From 33dce556e91c339b555129e3e4432ea48bdb9a4e Mon Sep 17 00:00:00 2001 From: vnikolova Date: Mon, 9 Feb 2026 16:39:25 +0100 Subject: [PATCH 4/7] reorganize registration and resolution sections and reference code from sample project --- topics/server-dependency-injection.md | 225 ++++++++++++++++---------- 1 file changed, 144 insertions(+), 81 deletions(-) diff --git a/topics/server-dependency-injection.md b/topics/server-dependency-injection.md index 52678ed20..fb06ff969 100644 --- a/topics/server-dependency-injection.md +++ b/topics/server-dependency-injection.md @@ -30,143 +30,206 @@ To use DI, include the `%artifact_name%` artifact in your build script: -## Basic dependency registration +## Dependency registration -You can register dependencies using lambdas, function references, or constructor references: +Ktor’s DI container needs to know how to create objects that your application depends on. This process is called +dependency registration. + +### Basic dependency registration + +Basic dependency registration is done in code using the `dependencies {}` block. You can register dependencies by +providing [lambdas](#lambda-registration), [function references](#function-reference), [class references](#class-reference), +or [constructor references](#constructor-reference): + +#### Use a lambda {id="lambda-registration"} + +Use a lambda when you want full control over how an instance is created: ```kotlin dependencies { - // Lambda-based provide { GreetingServiceImpl() } +} +``` +This registers a provider for `GreetingService`. Whenever `GreetingService` is requested, the lambda is executed to +create an instance. - // Function references +#### Use a constructor reference {id="constructor-reference"} + +If a class can be created using its constructor and all constructor parameters are already registered in the DI +container, you can use a constructor reference. + +```kotlin +dependencies { provide(::GreetingServiceImpl) +} +``` +This tells your application to use the constructor of `GreetingServiceImpl`, and let DI resolve its parameters. + +#### Use a class reference {id="class-reference"} + +You can register a concrete class without binding it to an interface: + +```kotlin +dependencies { provide(BankServiceImpl::class) +} +``` +In this case, the dependency is resolved by its `BankServiceImpl` type. +This is useful when the implementation type is injected directly and no abstraction is required. + +#### Use a function reference {id="function-reference"} + +You can register a function that creates and returns an instance: + +```kotlin +dependencies { provide(::createBankTeller) +} +``` + +The DI container resolves the function parameters and uses the return value as the dependency instance. - // Registering a lambda as a dependency - provide<() -> GreetingService> { { GreetingServiceImpl() } } +#### Use a factory lambda {id="factory-lambda-registration"} + +You can register a function itself as a dependency: + +```kotlin +dependencies { + provide<() -> GreetingService> { + { GreetingServiceImpl() } + } } ``` -## Configuration-based dependency registration +This registers a function that can be injected and called manually to create new instances. -You can configure dependencies declaratively using classpath references in your configuration file. This supports -both function and class references: +### Configuration-based dependency registration + +You can configure dependencies declaratively using classpath references in your configuration file. You can list a +function that returns an object, or a class with a resolvable constructor. + +List the dependencies under the `ktor.application.dependencies` group in your configuration file: + + + ```yaml -# application.yaml -ktor: - application: - dependencies: - - com.example.RepositoriesKt.provideDatabase - - com.example.UserRepository -database: - connectionUrl: postgres://localhost:3037/admin ``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-7"} -```kotlin -// Repositories.kt -fun provideDatabase(@Property("database.connectionUrl") connectionUrl: String): Database = - PostgresDatabase(connectionUrl) + + -class UserRepository(val db: Database) { - // implementation -} -``` +Ktor resolves function and constructor parameters automatically using the DI container. + +## Dependency resolution -Ktor resolves constructor and function parameters automatically using the DI container. You can use annotations like -`@Property` or `@Named` to override or explicitly bind parameters in special cases, such as when the type alone is not -enough to distinguish a value. If omitted, Ktor will attempt to resolve parameters by type using the DI container. +After you register dependencies, you can resolve them from the DI container and inject them into application code. -## Dependency resolution and injection +You can resolve dependencies explicitly from the DI container using either [property delegation](#property-delegation) +or [direct resolution](#direct-resolution). -### Resolving dependencies +### Use property delegation {id="property-delegation"} -To resolve dependencies, you can use property delegation or direct resolution: +When using property delegation, the dependency is resolved lazily when the property is first accessed: ```kotlin -// Using property delegation val service: GreetingService by dependencies +``` + +### Use direct resolution {id="direct-resolution"} + +Direct resolution returns the dependency immediately or suspends until it becomes available: -// Direct resolution +```kotlin val service = dependencies.resolve() ``` -### Asynchronous dependency resolution +### Parameter resolution -To support asynchronous loading, you can use suspending functions: +When resolving constructors or functions, Ktor resolves parameters using the DI container. Parameters are resolved by +type by default. -```kotlin -suspend fun Application.installEvents() { - val kubernetesConnection: EventsConnection = dependencies.resolve() // suspends until provided -} +If type-based resolution is insufficient, you can use annotations to explicitly bind parameters. -suspend fun Application.loadEventsConnection() { - dependencies.provide { - connect(property("app.events")) - } +#### Use named dependencies + +Use the `@Named` annotation to resolve a dependency registered with the specified name: + +```kotlin +fun Application.userRepository(@Named("mongo") database: Database) { + // Uses the dependency named "mongo" } ``` -The DI plugin will automatically suspend `resolve()` calls until all dependencies are ready. +#### Use configuration properties -### Injecting into application modules +Use the `@Property` annotation to inject a value from the application configuration: -You can inject dependencies directly into application modules by specifying parameters in the module function. Ktor -will resolve these dependencies from the DI container based on type matching. +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/Repositories.kt" include-symbol="provideDatabase"} -First, register your dependency providers in the `dependencies` section of the config: + +In the above example, the `database.connectionUrl` property is resolved from the application configuration: + + + ```yaml -ktor: - application: - dependencies: - - com.example.PrintStreamProviderKt.stdout - modules: - - com.example.LoggingKt.logging ``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-6,13-14"} -Here’s what the dependency provider and module function look like: + + -```kotlin -// com.example.PrintStreamProvider.kt -fun stdout(): () -> PrintStream = { System.out } -``` +### Asynchronous dependency resolution + +To support asynchronous loading, you can use suspending functions: ```kotlin -// com.example.Logging.kt -fun Application.logging(printStreamProvider: () -> PrintStream) { - dependencies { - provide { SimpleLogger(printStreamProvider()) } - } -} ``` +{src="snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt" include-lines="8-20"} -Use `@Named` for injecting specifically keyed dependencies: +The DI plugin will automatically suspend `resolve()` calls until all dependencies are ready. -```kotlin -fun Application.userRepository(@Named("mongo") database: Database) { - // Uses the dependency named "mongo" -} -``` +### Inject dependencies into application modules -### Property and configuration injection +You can inject dependencies directly into application modules by specifying parameters in the module function. Ktor +will resolve these dependencies from the DI container based on type matching. + +First, register your dependency providers in the `ktor.application.dependencies` group in your configuration file: -Use `@Property` to inject configuration values directly: + + ```yaml -connection: - domain: api.example.com - path: /v1 - protocol: https ``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-5,9-10,12"} + + + + +Define the dependency provider and module function with parameters for the dependencies you want injected: + + + + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt"} + + + ```kotlin -val connection: Connection = application.property("connection") ``` +{src="snippets/server-di/src/main/kotlin/com.example/Logging.kt"} + + + -This simplifies working with structured configuration and supports automatic parsing of primitive types. +You can then use the injected dependencies directly within the module function. ## Advanced dependency features @@ -175,10 +238,10 @@ This simplifies working with structured configuration and supports automatic par Use nullable types to handle optional dependencies gracefully: ```kotlin -// Using property delegation +// Uses property delegation val config: Config? by dependencies -// Or direct resolution +// Uses direct resolution val config = dependencies.resolve() ``` From 4d2b6abad742e06f54e71e237a659978da95b3e9 Mon Sep 17 00:00:00 2001 From: vnikolova Date: Mon, 9 Feb 2026 17:33:53 +0100 Subject: [PATCH 5/7] KTOR-8997 add docs for DI file configuration --- topics/server-dependency-injection.md | 92 +++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/topics/server-dependency-injection.md b/topics/server-dependency-injection.md index fb06ff969..f07d28985 100644 --- a/topics/server-dependency-injection.md +++ b/topics/server-dependency-injection.md @@ -336,6 +336,98 @@ dependencies { Dependencies are cleaned up in reverse order of declaration to ensure proper teardown. +## Configure the DI plugin + +You can configure the DI plugin in your application configuration file. These settings affect the behavior of +dependency resolution globally and apply to all registered dependencies. + +### Dependency key mapping + +The `ktor.di.keyMapping` property defines how dependency keys are generalized and matched during resolution. This +determines which registered dependencies are considered compatible when resolving a requested type. + +```yaml +ktor: + di: + keyMapping: Supertypes * Nullables * OutTypeArgumentsSupertypes * RawTypes +``` + +The above example matches the default key mapping used by the DI plugin. + +#### Available key mapping options + + + +<code>Default</code> +Uses the default combination: +Supertypes * Nullables * OutTypeArgumentsSupertypes * RawTypes + + +<code>Supertypes</code> +Allows resolving a dependency using any of its supertypes. + + +<code>Nullables</code> +Allows matching nullable and non-nullable variants of a type. + + +<code>OutTypeArgumentsSupertypes</code> +Allows covariance on out type parameters. + + +<code>RawTypes</code> +Allows resolving generic types without considering type arguments. + + +<code>Unnamed</code> +Ignores dependency names (@Named) when matching. + + + +#### Combine key mapping options + +You can combine key mapping options using set operators `*` (intersection), `+` (union) and `()` (grouping). + +In the following example, a dependency registered as `List` can be resolved as `Collection` (`Supertypes`), +`List` or `List?` (`RawTypes` and `Nullables`): + +```yaml +ktor: + di: + keyMapping: Supertypes + (Nullables * RawTypes) +``` + +It will not resolve as `Collection?`, because that combination is not included in the expression + +### Conflict resolution policy + +The `ktor.di.conflictPolicy` property controls how the DI container behaves when multiple providers are registered for +the same dependency key: + +```yaml +ktor: + di: + conflictPolicy: Default +``` + +#### Available policies + + + +<code>Default</code> +Throws an exception when a conflicting dependency is declared + + +<code>OverridePrevious</code> +Overrides the previous dependency with the newly provided one. + + +<code>IgnoreConflicts</code> +In test environments, the DI plugin uses IgnoreConflicts by default. This allows test code to override +production dependencies without triggering errors. + + + ## Testing with dependency injection The DI plugin provides tooling to simplify testing. You can override dependencies before loading your application From 989f3be571af7cc0ae95ce77fe01b383b515a923 Mon Sep 17 00:00:00 2001 From: vnikolova Date: Mon, 9 Feb 2026 19:07:52 +0100 Subject: [PATCH 6/7] split up DI sections into separate sub-topics --- ktor.tree | 9 +- topics/server-dependency-injection.md | 445 ++---------------- topics/server-di-configuration.md | 93 ++++ topics/server-di-dependency-registration.md | 94 ++++ topics/server-di-dependency-resolution.md | 169 +++++++ ...server-di-resource-lifecycle-management.md | 48 ++ topics/server-di-testing.md | 33 ++ 7 files changed, 472 insertions(+), 419 deletions(-) create mode 100644 topics/server-di-configuration.md create mode 100644 topics/server-di-dependency-registration.md create mode 100644 topics/server-di-dependency-resolution.md create mode 100644 topics/server-di-resource-lifecycle-management.md create mode 100644 topics/server-di-testing.md diff --git a/ktor.tree b/ktor.tree index 790027b68..a0ae555c0 100644 --- a/ktor.tree +++ b/ktor.tree @@ -54,7 +54,14 @@ accepts-web-file-names="configuration.html,configurations.html,environments.html,configuration-file.html"/> - + + + + + + + + diff --git a/topics/server-dependency-injection.md b/topics/server-dependency-injection.md index f07d28985..7fcfd430b 100644 --- a/topics/server-dependency-injection.md +++ b/topics/server-dependency-injection.md @@ -1,4 +1,4 @@ -[//]: # (title: Dependency Injection) +[//]: # (title: Dependency injection) @@ -19,10 +19,10 @@ runtime. This separation reduces coupling, improves testability, and makes it ea implementations without modifying existing code. Ktor provides a built‑in DI plugin that lets you register services and configuration objects once and access them -throughout your application. You can inject these dependencies into modules, plugins, routes, and other Ktor components -in a consistent, type‑safe way. The plugin integrates with the Ktor application lifecycle and supports scoping, -structured configuration, and automatic resource management, making it easier to organize and maintain application‑level -services. +throughout your application. You can [inject these dependencies into modules](server-di-dependency-resolution.md#inject-into-modules), +plugins, routes, and other Ktor components in a consistent, type‑safe way. The plugin integrates with the Ktor +application lifecycle and supports scoping, structured configuration, and [automatic resource management](server-di-resource-lifecycle-management.md), +making it easier to organize and maintain application‑level services. ## Add dependencies @@ -30,431 +30,40 @@ To use DI, include the `%artifact_name%` artifact in your build script: -## Dependency registration +## How dependency injection works in Ktor -Ktor’s DI container needs to know how to create objects that your application depends on. This process is called -dependency registration. +In Ktor, dependency injection is a single, integrated process that consists of two closely related steps: -### Basic dependency registration +* [Registering dependencies](server-di-dependency-registration.md) — declaring how instances are created. +* [Resolving dependencies](server-di-dependency-resolution.md) — accessing and injecting those instances at runtime. -Basic dependency registration is done in code using the `dependencies {}` block. You can register dependencies by -providing [lambdas](#lambda-registration), [function references](#function-reference), [class references](#class-reference), -or [constructor references](#constructor-reference): +These steps are handled by a single DI container. -#### Use a lambda {id="lambda-registration"} +To begin using dependency injection in your application, start with [registering dependencies](server-di-dependency-registration.md). +Once dependencies are declared, continue with [resolving dependencies](server-di-dependency-resolution.md). -Use a lambda when you want full control over how an instance is created: +## Supported features -```kotlin -dependencies { - provide { GreetingServiceImpl() } -} -``` -This registers a provider for `GreetingService`. Whenever `GreetingService` is requested, the lambda is executed to -create an instance. +The DI plugin supports a range of features intended to cover common application needs: -#### Use a constructor reference {id="constructor-reference"} +* [Type-safe dependency resolution](server-di-dependency-resolution.md). +* [Optional and nullable dependencies](server-di-dependency-resolution.md#optional-dependencies). +* [Covariant generic resolution](server-di-dependency-resolution.md#covariant-generics). +* [Asynchronous dependency resolution](server-di-dependency-resolution.md#async-dependency-resolution). +* [Automatic and custom resource lifecycle management](server-di-resource-lifecycle-management.md). -If a class can be created using its constructor and all constructor parameters are already registered in the DI -container, you can use a constructor reference. +## Configuration and lifecycle behavior -```kotlin -dependencies { - provide(::GreetingServiceImpl) -} -``` -This tells your application to use the constructor of `GreetingServiceImpl`, and let DI resolve its parameters. +The behavior of the DI container can be customized using configuration options. These options control how dependency +keys are matched, how conflicts are handled, and how resolution behaves in advanced scenarios. -#### Use a class reference {id="class-reference"} +For configuration details, see [](server-di-configuration.md). -You can register a concrete class without binding it to an interface: - -```kotlin -dependencies { - provide(BankServiceImpl::class) -} -``` -In this case, the dependency is resolved by its `BankServiceImpl` type. -This is useful when the implementation type is injected directly and no abstraction is required. - -#### Use a function reference {id="function-reference"} - -You can register a function that creates and returns an instance: - -```kotlin -dependencies { - provide(::createBankTeller) -} -``` - -The DI container resolves the function parameters and uses the return value as the dependency instance. - -#### Use a factory lambda {id="factory-lambda-registration"} - -You can register a function itself as a dependency: - -```kotlin -dependencies { - provide<() -> GreetingService> { - { GreetingServiceImpl() } - } -} -``` - -This registers a function that can be injected and called manually to create new instances. - -### Configuration-based dependency registration - -You can configure dependencies declaratively using classpath references in your configuration file. You can list a -function that returns an object, or a class with a resolvable constructor. - -List the dependencies under the `ktor.application.dependencies` group in your configuration file: - - - - -```yaml -``` -{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-7"} - - - - -Ktor resolves function and constructor parameters automatically using the DI container. - -## Dependency resolution - -After you register dependencies, you can resolve them from the DI container and inject them into application code. - -You can resolve dependencies explicitly from the DI container using either [property delegation](#property-delegation) -or [direct resolution](#direct-resolution). - -### Use property delegation {id="property-delegation"} - -When using property delegation, the dependency is resolved lazily when the property is first accessed: - -```kotlin -val service: GreetingService by dependencies -``` - -### Use direct resolution {id="direct-resolution"} - -Direct resolution returns the dependency immediately or suspends until it becomes available: - -```kotlin -val service = dependencies.resolve() -``` - -### Parameter resolution - -When resolving constructors or functions, Ktor resolves parameters using the DI container. Parameters are resolved by -type by default. - -If type-based resolution is insufficient, you can use annotations to explicitly bind parameters. - -#### Use named dependencies - -Use the `@Named` annotation to resolve a dependency registered with the specified name: - -```kotlin -fun Application.userRepository(@Named("mongo") database: Database) { - // Uses the dependency named "mongo" -} -``` - -#### Use configuration properties - -Use the `@Property` annotation to inject a value from the application configuration: - -```kotlin -``` -{src="snippets/server-di/src/main/kotlin/com.example/Repositories.kt" include-symbol="provideDatabase"} - - -In the above example, the `database.connectionUrl` property is resolved from the application configuration: - - - - -```yaml -``` -{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-6,13-14"} - - - - -### Asynchronous dependency resolution - -To support asynchronous loading, you can use suspending functions: - -```kotlin -``` -{src="snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt" include-lines="8-20"} - -The DI plugin will automatically suspend `resolve()` calls until all dependencies are ready. - -### Inject dependencies into application modules - -You can inject dependencies directly into application modules by specifying parameters in the module function. Ktor -will resolve these dependencies from the DI container based on type matching. - -First, register your dependency providers in the `ktor.application.dependencies` group in your configuration file: - - - - -```yaml -``` -{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-5,9-10,12"} - - - - -Define the dependency provider and module function with parameters for the dependencies you want injected: - - - - -```kotlin -``` -{src="snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt"} - - - - -```kotlin -``` -{src="snippets/server-di/src/main/kotlin/com.example/Logging.kt"} - - - - -You can then use the injected dependencies directly within the module function. - -## Advanced dependency features - -### Optional and nullable dependencies - -Use nullable types to handle optional dependencies gracefully: - -```kotlin -// Uses property delegation -val config: Config? by dependencies - -// Uses direct resolution -val config = dependencies.resolve() -``` - -### Covariant generics - -Ktor's DI system supports type covariance, which allows injecting a value as one of its supertypes when the type -parameter is covariant. This is especially useful for collections and interfaces that work with subtypes. - -```kotlin -dependencies { - provide> { listOf("one", "two") } -} - -// This will work due to type parameter covariance support -val stringList: List by dependencies -// This will also work -val stringCollection: Collection by dependencies -``` - -Covariance also works with non-generic supertypes: - -```kotlin -dependencies { - provide { BufferedOutputStream(System.out) } -} - -// This works because BufferedOutputStream is a subtype of OutputStream -val outputStream: OutputStream by dependencies -``` - -#### Limitations - -While the DI system supports covariance for generic types, it currently does not support resolving parameterized types -across type argument subtypes. That means you cannot retrieve a dependency using a type that is more specific or more -general than what was registered. - -For example, the following code will not resolve: - -```kotlin -dependencies { - provide> { CsqSink() } -} - -// Will not resolve -val charSequenceSink: Sink by dependencies -``` - -## Resource lifecycle management - -The DI plugin handles lifecycle and cleanup automatically when the application shuts down. - -### AutoCloseable support - -By default, any dependency that implements `AutoCloseable` is automatically closed when your application stops: - -```kotlin -class DatabaseConnection : AutoCloseable { - override fun close() { - // Close connections, release resources - } -} - -dependencies { - provide { DatabaseConnection() } -} -``` - -### Custom cleanup logic - -You can define custom cleanup logic by specifying a `cleanup` function: - -```kotlin -dependencies { - provide { ResourceManagerImpl() } cleanup { manager -> - manager.releaseResources() - } -} -``` - -### Scoped cleanup with key - -Use `key` to manage named resources and their cleanup: - -```kotlin -dependencies { - key("second") { - provide { CustomCloser() } - cleanup { it.closeMe() } - } -} -``` - -Dependencies are cleaned up in reverse order of declaration to ensure proper teardown. - -## Configure the DI plugin - -You can configure the DI plugin in your application configuration file. These settings affect the behavior of -dependency resolution globally and apply to all registered dependencies. - -### Dependency key mapping - -The `ktor.di.keyMapping` property defines how dependency keys are generalized and matched during resolution. This -determines which registered dependencies are considered compatible when resolving a requested type. - -```yaml -ktor: - di: - keyMapping: Supertypes * Nullables * OutTypeArgumentsSupertypes * RawTypes -``` - -The above example matches the default key mapping used by the DI plugin. - -#### Available key mapping options - - - -<code>Default</code> -Uses the default combination: -Supertypes * Nullables * OutTypeArgumentsSupertypes * RawTypes - - -<code>Supertypes</code> -Allows resolving a dependency using any of its supertypes. - - -<code>Nullables</code> -Allows matching nullable and non-nullable variants of a type. - - -<code>OutTypeArgumentsSupertypes</code> -Allows covariance on out type parameters. - - -<code>RawTypes</code> -Allows resolving generic types without considering type arguments. - - -<code>Unnamed</code> -Ignores dependency names (@Named) when matching. - - - -#### Combine key mapping options - -You can combine key mapping options using set operators `*` (intersection), `+` (union) and `()` (grouping). - -In the following example, a dependency registered as `List` can be resolved as `Collection` (`Supertypes`), -`List` or `List?` (`RawTypes` and `Nullables`): - -```yaml -ktor: - di: - keyMapping: Supertypes + (Nullables * RawTypes) -``` - -It will not resolve as `Collection?`, because that combination is not included in the expression - -### Conflict resolution policy - -The `ktor.di.conflictPolicy` property controls how the DI container behaves when multiple providers are registered for -the same dependency key: - -```yaml -ktor: - di: - conflictPolicy: Default -``` - -#### Available policies - - - -<code>Default</code> -Throws an exception when a conflicting dependency is declared - - -<code>OverridePrevious</code> -Overrides the previous dependency with the newly provided one. - - -<code>IgnoreConflicts</code> -In test environments, the DI plugin uses IgnoreConflicts by default. This allows test code to override -production dependencies without triggering errors. - - +For resource cleanup and shutdown behavior, see [](server-di-resource-lifecycle-management.md). ## Testing with dependency injection -The DI plugin provides tooling to simplify testing. You can override dependencies before loading your application -modules: - -```kotlin -fun test() = testApplication { - application { - dependencies.provide { - MockService() - } - loadServices() - } -} -``` - -### Loading configuration in tests - -Use `configure()` to load configuration files easily in your tests: - -```kotlin -fun test() = testApplication { - // Load properties from the default config file path - configure() - // Load multiple files with overrides - configure("root-config.yaml", "test-overrides.yaml") -} -``` +The DI plugin integrates with Ktor’s testing utilities and supports overriding dependencies, loading configuration, and +controlling conflict behavior in test environments. -Conflicting declarations are ignored by the test engine to let you override freely. +For more information and examples, see [](server-di-testing.md). diff --git a/topics/server-di-configuration.md b/topics/server-di-configuration.md new file mode 100644 index 000000000..8791bffcc --- /dev/null +++ b/topics/server-di-configuration.md @@ -0,0 +1,93 @@ +[//]: # (title: Configure the DI plugin) + + + +You can configure the dependency injection (DI) plugin in your application configuration file. These settings affect +the behavior of dependency resolution globally and apply to all registered dependencies. + +### Dependency key mapping + +The `ktor.di.keyMapping` property defines how dependency keys are generalized and matched during resolution. This +determines which registered dependencies are considered compatible when resolving a requested type. + +```yaml +ktor: + di: + keyMapping: Supertypes * Nullables * OutTypeArgumentsSupertypes * RawTypes +``` + +The above example matches the default key mapping used by the DI plugin. + +#### Available key mapping options + + + +<code>Default</code> +Uses the default combination: +Supertypes * Nullables * OutTypeArgumentsSupertypes * RawTypes + + +<code>Supertypes</code> +Allows resolving a dependency using any of its supertypes. + + +<code>Nullables</code> +Allows matching nullable and non-nullable variants of a type. + + +<code>OutTypeArgumentsSupertypes</code> +Allows covariance on out type parameters. + + +<code>RawTypes</code> +Allows resolving generic types without considering type arguments. + + +<code>Unnamed</code> +Ignores dependency names (@Named) when matching. + + + +#### Combine key mapping options + +You can combine key mapping options using set operators `*` (intersection), `+` (union) and `()` (grouping). + +In the following example, a dependency registered as `List` can be resolved as `Collection` (`Supertypes`), +`List` or `List?` (`RawTypes` and `Nullables`): + +```yaml +ktor: + di: + keyMapping: Supertypes + (Nullables * RawTypes) +``` + +It will not resolve as `Collection?`, because that combination is not included in the expression + +### Conflict resolution policy + +The `ktor.di.conflictPolicy` property controls how the DI container behaves when multiple providers are registered for +the same dependency key: + +```yaml +ktor: + di: + conflictPolicy: Default +``` + +#### Available policies + + + +<code>Default</code> +Throws an exception when a conflicting dependency is declared + + +<code>OverridePrevious</code> +Overrides the previous dependency with the newly provided one. + + +<code>IgnoreConflicts</code> +In test environments, the DI plugin uses IgnoreConflicts by default. This allows test code to override +production dependencies without triggering errors. + + \ No newline at end of file diff --git a/topics/server-di-dependency-registration.md b/topics/server-di-dependency-registration.md new file mode 100644 index 000000000..08ef55d17 --- /dev/null +++ b/topics/server-di-dependency-registration.md @@ -0,0 +1,94 @@ +[//]: # (title: Dependency registration) + + + +Ktor’s dependency injection (DI) container needs to know how to create objects that your application depends on. This +process is called dependency registration. + +### Basic dependency registration + +Basic dependency registration is done in code using the `dependencies {}` block. + +You can register dependencies by providing [lambdas](#lambda-registration), [function references](#function-reference), +[class references](#class-reference), or [constructor references](#constructor-reference): + +#### Use a lambda {id="lambda-registration"} + +Use a lambda when you want full control over how an instance is created: + +```kotlin +dependencies { + provide { GreetingServiceImpl() } +} +``` +This registers a provider for `GreetingService`. Whenever `GreetingService` is requested, the lambda is executed to +create an instance. + +#### Use a constructor reference {id="constructor-reference"} + +If a class can be created using its constructor and all constructor parameters are already registered in the DI +container, you can use a constructor reference. + +```kotlin +dependencies { + provide(::GreetingServiceImpl) +} +``` +This tells your application to use the constructor of `GreetingServiceImpl`, and let DI resolve its parameters. + +#### Use a class reference {id="class-reference"} + +You can register a concrete class without binding it to an interface: + +```kotlin +dependencies { + provide(BankServiceImpl::class) +} +``` +In this case, the dependency is resolved by its `BankServiceImpl` type. +This is useful when the implementation type is injected directly and no abstraction is required. + +#### Use a function reference {id="function-reference"} + +You can register a function that creates and returns an instance: + +```kotlin +dependencies { + provide(::createBankTeller) +} +``` + +The DI container resolves the function parameters and uses the return value as the dependency instance. + +#### Use a factory lambda {id="factory-lambda-registration"} + +You can register a function itself as a dependency: + +```kotlin +dependencies { + provide<() -> GreetingService> { + { GreetingServiceImpl() } + } +} +``` + +This registers a function that can be injected and called manually to create new instances. + +### Configuration-based dependency registration + +You can configure dependencies declaratively using classpath references in your configuration file. You can list a +function that returns an object, or a class with a resolvable constructor. + +List the dependencies under the `ktor.application.dependencies` group in your configuration file: + + + + +```yaml +``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-7"} + + + + +Ktor resolves function and constructor parameters automatically using the DI container. diff --git a/topics/server-di-dependency-resolution.md b/topics/server-di-dependency-resolution.md new file mode 100644 index 000000000..0a7b44bd9 --- /dev/null +++ b/topics/server-di-dependency-resolution.md @@ -0,0 +1,169 @@ +[//]: # (title: Dependency resolution) + + + +After you [register dependencies](server-di-dependency-registration.md), you can resolve them from the dependency +injection (DI) container and inject them into application code. + +You can resolve dependencies explicitly from the DI container using either [property delegation](#property-delegation) +or [direct resolution](#direct-resolution). + +### Use property delegation {id="property-delegation"} + +When using property delegation, the dependency is resolved lazily when the property is first accessed: + +```kotlin +val service: GreetingService by dependencies +``` + +### Use direct resolution {id="direct-resolution"} + +Direct resolution returns the dependency immediately or suspends until it becomes available: + +```kotlin +val service = dependencies.resolve() +``` + +### Parameter resolution + +When resolving constructors or functions, Ktor resolves parameters using the DI container. Parameters are resolved by +type by default. + +If type-based resolution is insufficient, you can use annotations to explicitly bind parameters. + +#### Use named dependencies + +Use the `@Named` annotation to resolve a dependency registered with the specified name: + +```kotlin +fun Application.userRepository(@Named("mongo") database: Database) { + // Uses the dependency named "mongo" +} +``` + +#### Use configuration properties + +Use the `@Property` annotation to inject a value from the application configuration: + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/Repositories.kt" include-symbol="provideDatabase"} + + +In the above example, the `database.connectionUrl` property is resolved from the application configuration: + + + + +```yaml +``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-6,13-14"} + + + + +### Asynchronous dependency resolution {id="async-dependency-resolution"} + +To support asynchronous loading, you can use suspending functions: + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/AsyncDependencies.kt" include-lines="8-20"} + +The DI plugin will automatically suspend `resolve()` calls until all dependencies are ready. + +### Inject dependencies into application modules {id="inject-into-modules"} + +You can inject dependencies directly into application modules by specifying parameters in the module function. Ktor +will resolve these dependencies from the DI container based on type matching. + +First, register your dependency providers in the `ktor.application.dependencies` group in your configuration file: + + + + +```yaml +``` +{src="snippets/server-di/src/main/resources/application.yaml" include-lines="1,4-5,9-10,12"} + + + + +Define the dependency provider and module function with parameters for the dependencies you want injected. You can then +use the injected dependencies directly within the module function: + + + + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/PrintStreamProvider.kt"} + + + + +```kotlin +``` +{src="snippets/server-di/src/main/kotlin/com.example/Logging.kt"} + + + + + +## Advanced dependency resolution + +### Optional and nullable dependencies {id="optional-dependencies"} + +Use nullable types to handle optional dependencies gracefully: + +```kotlin +// Uses property delegation +val config: Config? by dependencies + +// Uses direct resolution +val config = dependencies.resolve() +``` + +### Covariant generics {id="covariant-generics"} + +Ktor's DI system supports type covariance, which allows injecting a value as one of its supertypes when the type +parameter is covariant. This is especially useful for collections and interfaces that work with subtypes. + +```kotlin +dependencies { + provide> { listOf("one", "two") } +} + +// This will work due to type parameter covariance support +val stringList: List by dependencies +// This will also work +val stringCollection: Collection by dependencies +``` + +Covariance also works with non-generic supertypes: + +```kotlin +dependencies { + provide { BufferedOutputStream(System.out) } +} + +// This works because BufferedOutputStream is a subtype of OutputStream +val outputStream: OutputStream by dependencies +``` + +#### Limitations + +While the DI system supports covariance for generic types, it currently does not support resolving parameterized types +across type argument subtypes. That means you cannot retrieve a dependency using a type that is more specific or more +general than what was registered. + +For example, the following code will not resolve: + +```kotlin +dependencies { + provide> { CsqSink() } +} + +// Will not resolve +val charSequenceSink: Sink by dependencies +``` diff --git a/topics/server-di-resource-lifecycle-management.md b/topics/server-di-resource-lifecycle-management.md new file mode 100644 index 000000000..8d543b767 --- /dev/null +++ b/topics/server-di-resource-lifecycle-management.md @@ -0,0 +1,48 @@ +[//]: # (title: Resource lifecycle management) + + + +The dependency injection (DI) plugin handles lifecycle and cleanup automatically when the application shuts down. + +### AutoCloseable support + +By default, any dependency that implements `AutoCloseable` is automatically closed when your application stops: + +```kotlin +class DatabaseConnection : AutoCloseable { + override fun close() { + // Close connections, release resources + } +} + +dependencies { + provide { DatabaseConnection() } +} +``` + +### Custom cleanup logic + +You can define custom cleanup logic by specifying a `cleanup` function: + +```kotlin +dependencies { + provide { ResourceManagerImpl() } cleanup { manager -> + manager.releaseResources() + } +} +``` + +### Scoped cleanup with key + +Use `key` to manage named resources and their cleanup: + +```kotlin +dependencies { + key("second") { + provide { CustomCloser() } + cleanup { it.closeMe() } + } +} +``` + +Dependencies are cleaned up in reverse order of declaration to ensure proper teardown. diff --git a/topics/server-di-testing.md b/topics/server-di-testing.md new file mode 100644 index 000000000..8bcbe28b3 --- /dev/null +++ b/topics/server-di-testing.md @@ -0,0 +1,33 @@ +[//]: # (title: Testing with dependency injection) + + + +The dependency injection (DI) plugin provides tooling to simplify testing. + +You can override dependencies before loading your application modules: + +```kotlin +fun test() = testApplication { + application { + dependencies.provide { + MockService() + } + loadServices() + } +} +``` + +### Loading configuration in tests + +Use `configure()` to load configuration files easily in your tests: + +```kotlin +fun test() = testApplication { + // Load properties from the default config file path + configure() + // Load multiple files with overrides + configure("root-config.yaml", "test-overrides.yaml") +} +``` + +Conflicting declarations are ignored by the test engine to let you override freely. From 6fece4462f18a0b1366e9ca90847f63456eb265a Mon Sep 17 00:00:00 2001 From: vnikolova Date: Tue, 10 Feb 2026 17:51:02 +0100 Subject: [PATCH 7/7] KTOR-9294 expand docs and examples of named dependencies --- codeSnippets/snippets/server-di/requests.http | 22 +++++++ .../src/main/kotlin/com.example/Logging.kt | 2 +- .../main/kotlin/com.example/PaymentService.kt | 62 +++++++++++++++++++ .../kotlin/com.example/QualifiersExample.kt | 14 ----- .../src/main/resources/application.yaml | 8 ++- topics/server-di-configuration.md | 12 +++- topics/server-di-dependency-registration.md | 31 +++++++++- topics/server-di-dependency-resolution.md | 12 +++- ...server-di-resource-lifecycle-management.md | 10 ++- topics/server-di-testing.md | 10 ++- 10 files changed, 159 insertions(+), 24 deletions(-) create mode 100644 codeSnippets/snippets/server-di/requests.http create mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/PaymentService.kt delete mode 100644 codeSnippets/snippets/server-di/src/main/kotlin/com.example/QualifiersExample.kt diff --git a/codeSnippets/snippets/server-di/requests.http b/codeSnippets/snippets/server-di/requests.http new file mode 100644 index 000000000..ea4fdd0de --- /dev/null +++ b/codeSnippets/snippets/server-di/requests.http @@ -0,0 +1,22 @@ +@host = http://localhost:8080 + +### Successful checkout using cookies and amount +POST http://localhost:8080/checkout?amount=1500 +Content-Type: application/x-www-form-urlencoded +Cookie: userId=alice; cartId=cart123 + +### + +### Missing userId cookie +POST http://localhost:8080/checkout?amount=1500 +Content-Type: application/x-www-form-urlencoded +Cookie: cartId=cart-123 + +### + +### Missing amount query parameter +POST http://localhost:8080/checkout +Content-Type: application/x-www-form-urlencoded +Cookie: userId=alice; cartId=cart-123 + +### diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt index c4784d97f..cd4acca94 100644 --- a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/Logging.kt @@ -4,7 +4,7 @@ import io.ktor.server.application.* import io.ktor.server.plugins.di.dependencies import java.io.PrintStream - class Logger(private val out: PrintStream) { +class Logger(private val out: PrintStream) { fun log(message: String) { out.println("[LOG] $message") } diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PaymentService.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PaymentService.kt new file mode 100644 index 000000000..b78998b91 --- /dev/null +++ b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/PaymentService.kt @@ -0,0 +1,62 @@ +package com.example + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.di.annotations.* +import io.ktor.server.plugins.di.dependencies +import io.ktor.server.response.* +import io.ktor.server.routing.* + +interface PaymentProcessor { + suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) +} +class CreditCardPaymentProvider( + val baseUrl: String, + val clientKey: String, + val hashEncoding: (String) -> String +) : PaymentProcessor { + override suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) { + call.response.header("X-Transaction-Id", "$userId:$cartId") + call.response.header("X-Digest", hashEncoding("$clientKey:$userId:$cartId:$amount")) + call.respondRedirect("$baseUrl/payment/$amount") + } +} + +class PointsBalancePaymentProvider( + val updatePoints: suspend (String, Long) -> Long +) : PaymentProcessor { + override suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) { + updatePoints(userId, amount) + call.respondRedirect("/paymentComplete?userId=$userId&cartId=$cartId&amount=$amount") + } +} + +fun Application.configureExternalPaymentProvider( + @Property("payments.url") baseUrl: String, + @Property("payments.clientKey") clientKey: String, +) { + dependencies { + provide("external") { CreditCardPaymentProvider(baseUrl, clientKey) { it.reversed() } } + } +} + + +fun Application.paymentsHandling( + @Named("external") payments: PaymentProcessor +) { + log.info("Using payment processor: $payments") + routing { + post("/checkout") { + val userId = call.request.cookies["userId"] + ?: return@post call.respondText("Login required", status = HttpStatusCode.Forbidden) + val cartId = call.request.cookies["cartId"] + ?: return@post call.respondText("Cart ID missing", status = HttpStatusCode.Forbidden) + val amount = call.request.queryParameters["amount"]?.toLongOrNull() ?: return@post call.respondText("Amount missing", status = HttpStatusCode.BadRequest) + + payments.handlePayment(call, userId, cartId, amount) + } + get("/paymment/{amount}") { + call.respondText("Payment for ${call.parameters["amount"]} is pending...") + } + } +} \ No newline at end of file diff --git a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/QualifiersExample.kt b/codeSnippets/snippets/server-di/src/main/kotlin/com.example/QualifiersExample.kt deleted file mode 100644 index 936533f0a..000000000 --- a/codeSnippets/snippets/server-di/src/main/kotlin/com.example/QualifiersExample.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.example - -import io.ktor.server.application.* -import io.ktor.server.plugins.di.annotations.Named - -interface PaymentProcessor -class MongoPaymentProcessor : PaymentProcessor -class SqlPaymentProcessor : PaymentProcessor - -fun Application.paymentModule( - @Named("mongo") mongo: PaymentProcessor -) { - log.info("Using payment processor: $mongo") -} diff --git a/codeSnippets/snippets/server-di/src/main/resources/application.yaml b/codeSnippets/snippets/server-di/src/main/resources/application.yaml index 1d96e4ca1..4b8a92dae 100644 --- a/codeSnippets/snippets/server-di/src/main/resources/application.yaml +++ b/codeSnippets/snippets/server-di/src/main/resources/application.yaml @@ -10,10 +10,16 @@ ktor: modules: - com.example.ApplicationKt.module - com.example.LoggingKt.logging + - com.example.PaymentServiceKt.configureExternalPaymentProvider + - com.example.PaymentServiceKt.paymentsHandling database: connectionUrl: postgres://localhost:3037/admin connection: domain: api.example.com path: /v1 - protocol: https \ No newline at end of file + protocol: https + +payments: + url: "http://localhost:8080" + clientKey: "super-secret-client-key" \ No newline at end of file diff --git a/topics/server-di-configuration.md b/topics/server-di-configuration.md index 8791bffcc..dec7a29f0 100644 --- a/topics/server-di-configuration.md +++ b/topics/server-di-configuration.md @@ -2,7 +2,15 @@ -You can configure the dependency injection (DI) plugin in your application configuration file. These settings affect + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +You can configure the [dependency injection (DI) plugin](server-dependency-injection.md) in your application configuration file. These settings affect the behavior of dependency resolution globally and apply to all registered dependencies. ### Dependency key mapping @@ -61,7 +69,7 @@ ktor: keyMapping: Supertypes + (Nullables * RawTypes) ``` -It will not resolve as `Collection?`, because that combination is not included in the expression +It will not resolve as `Collection?`, because that combination is not included in the expression. ### Conflict resolution policy diff --git a/topics/server-di-dependency-registration.md b/topics/server-di-dependency-registration.md index 08ef55d17..590f21cbd 100644 --- a/topics/server-di-dependency-registration.md +++ b/topics/server-di-dependency-registration.md @@ -2,12 +2,21 @@ -Ktor’s dependency injection (DI) container needs to know how to create objects that your application depends on. This + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +Ktor’s [dependency injection (DI)](server-dependency-injection.md) container needs to know how to create objects that your application depends on. This process is called dependency registration. ### Basic dependency registration -Basic dependency registration is done in code using the `dependencies {}` block. +Basic dependency registration is done in code, typically within an `Application` module using the `dependencies {}` +block. You can register dependencies by providing [lambdas](#lambda-registration), [function references](#function-reference), [class references](#class-reference), or [constructor references](#constructor-reference): @@ -74,6 +83,24 @@ dependencies { This registers a function that can be injected and called manually to create new instances. +### Named dependency registration {id="named-registration"} + +You can assign a name to a dependency at registration time to distinguish multiple providers of the same type. + +This is useful when you need to register more than one implementation or instance for a single type and select between +them explicitly during resolution. + +To assign a name to a dependency, pass the name as the first argument to the `provide()` function: + +```kotlin +dependencies { + provide("default") { GreetingServiceImpl() } + provide("alternative") { AlternativeGreetingServiceImpl() } +} +``` + +Named dependencies must be [resolved explicitly using the `@Named` annotation](server-di-dependency-resolution.md#resolve-named). + ### Configuration-based dependency registration You can configure dependencies declaratively using classpath references in your configuration file. You can list a diff --git a/topics/server-di-dependency-resolution.md b/topics/server-di-dependency-resolution.md index 0a7b44bd9..8818a4972 100644 --- a/topics/server-di-dependency-resolution.md +++ b/topics/server-di-dependency-resolution.md @@ -2,6 +2,14 @@ + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ After you [register dependencies](server-di-dependency-registration.md), you can resolve them from the dependency injection (DI) container and inject them into application code. @@ -31,9 +39,9 @@ type by default. If type-based resolution is insufficient, you can use annotations to explicitly bind parameters. -#### Use named dependencies +#### Use named dependencies {id="resolve-named"} -Use the `@Named` annotation to resolve a dependency registered with the specified name: +Use the `@Named` annotation to resolve a dependency [registered with a specified name](server-di-dependency-registration.md#named-registration): ```kotlin fun Application.userRepository(@Named("mongo") database: Database) { diff --git a/topics/server-di-resource-lifecycle-management.md b/topics/server-di-resource-lifecycle-management.md index 8d543b767..25661b379 100644 --- a/topics/server-di-resource-lifecycle-management.md +++ b/topics/server-di-resource-lifecycle-management.md @@ -2,7 +2,15 @@ -The dependency injection (DI) plugin handles lifecycle and cleanup automatically when the application shuts down. + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +The [dependency injection (DI) plugin](server-dependency-injection.md) handles lifecycle and cleanup automatically when the application shuts down. ### AutoCloseable support diff --git a/topics/server-di-testing.md b/topics/server-di-testing.md index 8bcbe28b3..6a5ecdaa7 100644 --- a/topics/server-di-testing.md +++ b/topics/server-di-testing.md @@ -2,7 +2,15 @@ -The dependency injection (DI) plugin provides tooling to simplify testing. + +

+Required dependencies: io.ktor:ktor-server-di +

+ + +
+ +The [dependency injection (DI) plugin](server-dependency-injection.md) provides tooling to simplify testing. You can override dependencies before loading your application modules: