I am not going into details about which purpose a microservice approach fulfills, nor do I spend too much time in microservice architecture theory. The starting point of this post is that you want to improve your existing microservice landscape or you want to migrate to one by improving user and/or administration experience using a Web UI. In the best case, you are already familiar with Kotlin.
My team and I were developing huge, monolithic Java EE backend applications within one of our projects over the last few years for one of our customers. With the years, we developed concepts how to cut these applications into several microservices and how to shift the whole business into the Cloud. These included: Technological and organizational transformation, studying and practicing new technologies and technology approaches, handling the old and the new world in parallel while the overall business increases and changes.
These applications had embedded JSF Web UIs, serving several administrative purposes. Reading and manipulating state, triggering actions, retrieving data for analysis or debugging purposes, etc. This had significant advantages.
Above all, next to the developer who had an “easy way” to work with the running applications, the customer could do that easily as well. Current database state could be accessed without any database knowledge – including prevention of database access management for non-developers. Event or synchronization actions could be displayed or triggered without technical knowledge. Therefore, the target was that within the upcoming microservice landscape, these Web UI capabilities should somehow remain.
Before we continue, some concepts.
Microservice without an UI
The question before starting: Does your (micro-)service even need a UI? As always, it depends. For instance, if your service does not have anything “to show”, like a persistence or internal state, or if it is not worth spending time and energy on UI development where a simple API endpoint would be sufficient, then you probably go without a UI. Another point: Do you have the capacity and knowledge available for UI development?
An example: A customer-facing, monolithic Web UI (Angular, React or similar) backed by several backend endpoints. Related terms are “rich” or “thick” clients.
Instead of having one rich UI, we have several UI components stuck together. How this component’s development is organized or orchestrated depends. Mono versus multiple repositories – one versus independent teams. Terms like “Microfrontend” fit here.
Self-contained System (embedded UI)
Within the self-contained system approach, we isolate the microservice and its UI capabilities. This means, the UI and the service itself usually run in the same process and are usually one deployable unit. I will follow this approach in this article as it perfectly fits with Kotlin Multiplatform.
We could build our microservice and our UI using classic approaches like having our JVM-Code in one and our UI-stuff in another, distinct and isolated packages of our service – sharing nothing but equal network configs. This approach works well, for example with a Spring-Kotlin service exposing a Vue.js UI. Build the frontend output into the class path of your backend and expose it using a REST controller. It does work well, but can we improve?
The point is, as a developer, you live here in two different worlds. On the one hand, you have your backend code with its own ecosystem, and on the other hand, you have the frontend related stuff with a completely different one. For instance, data models, endpoints, constants, functions, tests, libraries, tools, etc. both share equally, must be maintained twice and in different languages – even if they live in the same repository. It sounds annoying in the long run… and it is from my experience. Here we could improve with Multiplatform approaches.
With Kotlin Multiplatform you can extract the common code – backend and frontend share – building a bridge to both worlds. You define your common, internal models, endpoints, constants, functions, tests, etc. in a central package or module and your backend and frontend are always in sync, sharing the same base. You share code between platforms. Another advantage: Using Kotlin Multiplatform, you must not leave Kotlin!
Support for multiplatform programming is one of Kotlin’s key benefits. It reduces time spent on writing and maintaining the same code for different platforms while retaining the flexibility and benefits of native programming.
Kotlin is a great choice for the development of JVM applications and it is already very well known here. From developing services from scratch, rewriting legacy Java code, to integrating new Kotlin code into the existing Java base. While Kotlin works very well for backend development, the concept of developing frontends in Kotlin is quite new. Several Kotlin wrappers are maintained by JetBrains – like React, Mocha or styled-components – and further community solutions exist. Gradle offers several ways to control and bundle using webpack integrating with npm using yarn.
My sample service can be found on Github: https://github.com/jbilandzija/kotlin-multiplatform-sample
Here I use a very simple backend based on Ktor and a very simple frontend using KotlinJS and React. In case you have never heard of Ktor, Ktor is a great alternative in the area of JVM microframeworks to create lightweight services – and it integrates perfectly with Kotlin and Coroutines! If you prefer going on with Spring or any other preferred framework – you are free to go.
The service consists of three source sets (excluding tests): jvmMain, jsMain and commonMain. These source sets (or modules) are defined in the main Gradle file. Other alternative setups exist. We separate into different modules as we compile to different targets: JVM and JS. We have dedicated Ktor/Server dependencies and dedicated KotlinJS/Client ones.
After compilation, we get two artifacts. To combine both into one single, fat JAR I use the Shadow plugin. This single JAR can be handled as any other (micro-)service JAR we would have without an embedded UI. Make a container image out of it, push it to your favorite Platform-as-a-Service, or similar.
Building the UI
My example is structured into two functional components, InputComponent and OutputComponent. Within the InputComponent structure, the look and behavior of the Input section are defined. The OutputComponent displays the server output.
How to structure your components is up to you and your team. I followed the approach to define variables and handlers at the top, followed by structure and nested CSS settings. See example.
kotlin-test-js module – a JS implementation of common test assertions and annotations with the out-of-the-box support for Jasmine, Mocha, and Jest testing frameworks, and an experimental way to plug in a custom unit testing framework.
The backend in my example has two endpoints for the Web UI, configured within the routing plugin. A root endpoint exposing the index.html and static resources including the main JS file (kt-multiplatform-sample.js). The second endpoint is the endpoint our Web UI calls. This endpoint path is configured centrally. Server and client are using the same constant. Additional benefit: No need to care about API path renaming or API versioning 😉.
It is hard to conclude or to give final decisions about a technology that is still an alpha state. But, is Kotlin Multiplatform ready to build full-stack (web-)services? For me, it is not yet. Let me state why.
I have been experimenting with the hands-on example above and a very similar project for a while now. It is in the nature of very young technologies that a lot changes after new dependency releases – especially technology in alpha state. It took me a while until I built a service that worked and looked the way I wanted, as Kotlin Multiplatform was new to me. After new dependency releases, I had to rework a lot until my service worked and looked as before.
If you have Kotlin microservices and you plan to extend them with an embedded UI, keep Kotlin Multiplatform definitely in mind. I would recommend waiting until Kotlin Multiplatform and its ecosystem evolve further.