Our adventure using Play Framework with Kotlin
Six months ago we started a Play Framework project using Kotlin, and we wanted to share with you this extraordinary adventure. We faced exciting challenges we'd like to share with you using this blog post, from the project configuration with Kotlin to supporting automated tests or using Arrow to play with functional programming. We decided to use Kotlin and Play Framework because we love how powerful this language is and the versatility it provided us. Are you ready to start this adventure with us?
In the beginning, we didn’t know if we were going to be able to do it because Play Framework doesn't have native support for Kotlin. One day, while working at the Karumi offices, we found kotlin-plugin, a plugin for compiling Kotlin with sbt. The first thing we did was to try this sbt plugin in a pet project. We found that this plugin didn't fulfill our needs because of the lack of testing support. To solve this issue we forked the original project and added the missing feature. You can find the outcome here: kotlin-plugin#24. Another issue we found was that we were forced to use version 1.1.x of Kotlin, however, this wasn't a blocker for our goals.
Once we had the base of the project ready, it was time to start with the scaffolding. Every professional project must be configured with a linter, provide support for different automated testing strategies, and be configured with some libraries, letting us code using our favorite paradigm or coding style.
The usage of a linter was required to ensure every developer in the project was following the same code style. By that time, we were used to Ktlint. We thought this linter was a perfect tool for our project, but sbt didn't support it. We couldn't find any sbt plugin, so we decided to implement our own. Even when you can download the binary and run it from your computer, we needed to integrate this tool into our CI environment, and that's why we finally decided to implement it ourselves. You can find the result here: Ktlint-sbt
When talking about the automatic testing support, we needed to write plain old unit tests and also integration tests using Kotlin. The coverage of our controllers and repositories among our business logic was crucial. Thanks to Kotlin and different testing strategies we could write tests using an embedded version of the database and a dependency injector that let us replace production code with tests doubles when needed. You can review this approach in a blog post we published: Testing with H2 in play framework.
Once the tooling we needed was up and running, it was time to code! For this project we decided to use functional programming data types such as Option
, Either
, Try
, or Validated
. Thanks to Arrow, a functional programming library for Kotlin, we could easily represent the possible state of the software execution flow under control. You can find how we used this library in this example project we wrote: A play framework example with Kotlin. For those who are not used to reading functional programming code, we decided to use every function with the named params the function declares. This simplifies how the code is read for developers who don't know how a fold
or flatMap
function looks like.
To show how we combined all the already mentioned strategies, we are going to show you some code snippets extracted from this project you can find in this GitHub repository: play-framework-kotlin.
Controllers
We have added a little bit of Kotlin magic to be able to make our controllers more concise. Thanks to the extension functions, we have created this small function, it helps to declare asynchronous controllers methods.
fun async(complete: () -> Result): CompletionStage<Result> =
CompletableFuture.supplyAsync(complete)
Here you have an example of the usage of this extension:
fun getDeveloper(id: String): CompletionStage<Result> = async {
//it runs asynchronous
}
Another example would be to simplify the JSON parser from the body request context and transform it into an object that we specified in the generic type.
fun createDeveloper(): CompletionStage<Result> = readAsyncJsonBody<NewDeveloperJson> { newDeveloperJson ->
//outside play framework context
}
Business logic public API signature
Thanks to the usage of abstractions, all our business logic methods have a very similar structure based on a single abstraction, we have been able to define methods to handle the results of the request in our controller in the same way. Our use cases return an Either
value depending on if it is an error (left side) or it is a success (right side). Each method knows how it has to transform the result of the operation. Here you can find the implementation of Either
in Arrow.
In this following snippet, you can find an example of the different methods implemented in our controllers:
fun createDeveloper(): CompletionStage<Result> = readAsyncJsonBody<NewDeveloperJson> {
createKarumiDeveloper(it.toDomain()).fold(
ifLeft = this::processError,
ifRight = this::created
)
}
fun getDeveloper(developerId: String): CompletionStage<Result> = async {
getDeveloper(UUID.fromString(developerId)).fold(
ifLeft = this::processError,
ifRight = this::ok
)
}
private fun processError(error: DeveloperError): Result = when (error) {
DeveloperError.StorageError -> internalServerError()
DeveloperError.NotFound -> notFound()
DeveloperError.NotKarumier -> badRequest("Only karumies")
}
private fun created(dev: Developer): Result = Results.created(dev.toJson())
private fun ok(dev: Developer): Result = Results.ok(dev.toJson())
Storage
To use PostgreSQL as the database, we decided to write our DAOs code using Ebean-ORM, which has Kotlin support. This ORM allows us to handle the database easily, the only thing we need to take care of is to avoid sharing implementation details with our domain code.
As we were using a third-party library which could throw an exception we decided to wrap it in a Try
value which, like Either
, has a failure side and success side, you have more information in the Arrow documentation. When the code throws an exception, Try
will catch it and thanks to its methods we can deal with the exception without using any try catch
statement.
This is an example of the usage of the database handling using the Try
constructor:
fun getById(id: UUID): Try<Option<Developer>> = Try {
DeveloperEntity.DAO
.query()
.where()
.idEq(id)
.findOne()
?.toDomain()
.toOption()
}
Domain
While implementing our business logic we decided to handle the output of our functions deciding what to do depending on the return value of a Try
, if it is a success we transform it to a Right
of the Either
, when it is a failure we transform it to a Left
with the corresponding domain error. It is very common to combine data very easily using the functor methods like flatmap
, map
, fold
, and they are usually the most used. This allows us to write declarative code in all the workflows. The usage of these abstractions with the compiler help will enable us to identify all the possible scenarios of our software easily.
Here you can find a segment of code that is in our domain:
class CreateKarumiDeveloper @Inject constructor(
private val developerDao: DeveloperDao
) {
operator fun invoke(developer: Developer): Either<DeveloperError, Developer> =
validKarumiDeveloper(developer)
.flatMap {
developerDao.create(it)
.toEither()
.mapLeft { DeveloperError.StorageError }
}
private fun validKarumiDeveloper(developer: Developer): Either<DeveloperError, Developer> =
if (DeveloperValidator.isKarumiDeveloper(developer)) {
developer.right()
} else {
DeveloperError.NotKarumier.left()
}
}
Automated Testing
However, what about automated testing? We can’t consider a project finished with the expected quality of a professional team without a good battery automated tests. We used JUnit and all Java tools with Kotlin. This kind of tooling is really powerful and using Kotlin we can use it in a friendly and simple way thanks to its syntax. The tests that we usually write are:
- Controller tests mocking the use cases thanks to the usage of the Dependency Injector.
- API tests using stubbing HTTP with Wiremock.
- Integration test providing embedded versions of the databases like H2 and Elasticsearch.
- Unit test using test doubles when it is needed.
Being able to use all these different testing strategies wasn't easy, but we are happy with the result. Today we can write any service with a language we love with a Framework which it gives us much versatility. We cover all automated test parts besides we can use static code analysis tools easily. What else would we need?
Conclusion
We hope you can join to our adventure and this blog post could be the beginning to grow the Kotlin community in Play Framework writing your services. Don’t be afraid to face new challenges; we are sure you’ll get it! To be sure you aren’t going to have any problem we arrange some tools to help you on the way:
Good luck!