Ktor: Crafting a Stock Portfolio Endpoint – Part 2.5

Ktor: Crafting a Stock Portfolio Endpoint – Part 2.5
Generated by ChatPGT

Before we delve into Part 3, which focuses on unit testing, it's essential to address one key preparation step to improve the testability and reusability of the code: dependency injection. This post directly follows Part 2 of a series of posts that aim to build a single endpoint with Ktor by wrapping the AlphaVantage Quote Endpoint. I recommend at least skimming through the previous posts if you haven't read them yet, as it will provide useful context. Having said that, if you're already caught up with the series, let's dive into dependency injection.

Why Dependency Injection?

In simple terms, dependency injection is a process in which objects or functions are injected (or passed) into another object or function instead of being created internally. As the concept of dependency injection has been extensively covered in other articles and videos, this section will specifically focus on its application in our server codebase. Upon revisiting the codebase, you may notice the following line of code.

fun Application.configureRouting() {
    //...
    val api = AlphaVantageApiImpl()
    //...
}

Routing.kt

Remember how we used the Bridge design pattern for testability in Part 2? However, the AlphaVantageApi interface hasn't been utilized until now. Instead of creating the AlphaVantageApiImpl internally inside configureRouting, we can pass the interface as a parameter.

fun Application.configureRouting(
    api: AlphaVantageApi
) {
    //...
}

Routing.kt

Imagine there are other implementations of the AlphaVantageApi (e.g. AlphaVantageComplexApiImpl). The new configureRouting delegates the creation of AlphaVantageApi and should work just fine regardless of the specific implementations of AlphaVantageApi. Thus, the api is injected into the configureRouting instead of created internally.

Another advantage of dependency injection is testability. To test the configureRouting, the api should be mocked and return fake data. Otherwise, the value of the real endpoint changes based on the real stock data, which makes it extremely hard to maintain the tests.

Koin Setup

In a project of this scale, the full potential of using a dependency injection framework might not be immediately apparent. For this tutorial, we'll explore Koin, a tool I've not used previously for the sake of learning together. Another great reason to choose Koin is its great documentation when it comes to setting Koin up in a Ktor project.

Similar to adding any other libraries, we need to add the dependencies first.

val koinVersion: String by project
//...

dependencies {
  //...
  implementation("io.insert-koin:koin-ktor:$koinVersion")
  implementation("io.insert-koin:koin-test:$koinVersion")
  implementation("io.insert-koin:koin-logger-slf4j:$koinVersion")
}

build.gradle.kts

To specify the koinVersion mentioned above, we need to adjust gradle.properties file.

//...
koinVersion=3.5.3 // Note: The latest stable version of Koin might differ when you read this

gradle.properties

Following Koin documentation for Ktor, the next step is to install the Koin plugin with the following code.

fun Application.configureKoin() {
    install(Koin) {
        slf4jLogger()
        modules(applicationModule)
    }
}

Koin.kt

Remember to call this new function in the Application.kt itself.

//...
fun Application.module() {
    //...
    configureKoin()
}

Application.kt

There is still one problem (compilation error) because applicationModule isn't defined yet. For that reason, let's learn more about modules to create them.

Modules

Once again, the Koin documentation offers comprehensive information about modules in its Definitions section. In short, a Koin module is a logical space to organize the definitions and configurations for the dependency injection. It requires minimal code to define a module. Given the simplicity of our server, consolidating all dependency injection logic into a single module is the most reasonable approach. Let's create an empty applicationModule in a new file to fix the previous problem.

//...
val applicationModule = module {
  // Intentionally left empty
}

ApplicationModule.kt

After importing applicationModule in Koin.kt, our code is ready to run smoothly again with the proper Koin setup.

Injection

Up to this point, our focus has been on preparatory steps. Whenever we need to inject an object, the following steps are the only necessary ones.

First, we need to determine which object to inject. In our case, it is the AlphaVantageApiImpl as discussed in the first section. With the following setup, Koin will provide AlphaVantageApiImpl whenever AlphaVantageApi is required.

//...
val applicationModule = module {
    single<AlphaVantageApi> {
        AlphaVantageApiImpl()
    }
}

ApplicationModule.kt

The keyword single indicates that the provided AlphaVantageApi is a Singleton. In practical terms, this ensures that every time AlphaVantageApi is requested, the same instance of AlphaVantageApiImpl is used. In order to provide AlphaVantageApi, simply add this code to Application.kt.

//...
fun Application.module() {
  //...
  val api by inject<AlphaVantageApi>()
  configureRouting(api = api)
}

Application.kt

Remember to change the configureRouting function to receive the api parameter as discussed in the first section for the code to work.

Conclusion

While the core functionality of the code remains unchanged, the design pattern has been significantly enhanced for better testability and maintainability. I hope this post has provided you with a clearer understanding of dependency injection in a Ktor project. Don’t forget to share your thoughts or questions below, and make sure to subscribe to our newsletter to not miss out on Part 3, which will cover unit testing!

GitHub Repository

GitHub - trinhan-nguyen/ktor-ferreter
Contribute to trinhan-nguyen/ktor-ferreter development by creating an account on GitHub.

Reference

Inversion of Control Containers and the Dependency Injection pattern
Explaining the Dependency Injection pattern, by contrasting it with Service Locator. The choice between them is less important than the principle of separating configuration from use.
What is dependency injection in object-oriented programming (OOP)? – TechTarget Definition
Dependency injection in OOP supplies resources required by a piece of code. Learn types, its roles, relationship to inversion of control and pros/cons.
Dependency Injection in Ktor | Koin
The koin-ktor module is dedicated to bring dependency injection for Ktor.
Ktor: Crafting a Stock Portfolio Endpoint – Part 2
This is the second post in the series of posts that aim to build a single endpoint with Ktor by wrapping the AlphaVantage Quote Endpoint.