
Currently, there is no shortage of frameworks for creating microservices in Java and Kotlin.
The article discusses the following:
Based on them, four services have been created that can interact with each other using the HTTP API using the Service Discovery pattern implemented using
Consul . Thus, they form a heterogeneous (at the level of frameworks) microservice architecture (hereafter referred to as ISA):
')

Define a set of requirements for each service:
- technology stack:
- JDK 12;
- Kotlin;
- Gradle (Kotlin DSL);
- JUnit 5.
- functionality (HTTP API):
GET /application-info{?request-to=some-service-name}
Returns some basic information about microservice (name, framework, year of release of the framework); when specifying the name of one of the four microservices in the request-to
parameter to its HTTP API, a similar request is executed that returns basic information;GET /application-info/logo
Returns an image.
- implementation:
- configuration using the configuration file;
- use dependency injection;
- tests that test the HTTP API.
- MSA:
- using the Service Discovery pattern (registration with Consul, accessing the HTTP API of another microservice by its name using client load balancing);
- uber-jar artifact formation.
Next, we consider the implementation of microservice on each of the frameworks and compare the parameters of the resulting applications.
Helidon service
The development framework was created in Oracle for internal use, subsequently becoming open-source. There are two development models based on this framework: Standard Edition (SE) and MicroProfile (MP). In both cases, the service will be a regular Java SE program. Learn more about the differences on
this page.
In short, Helidon MP is one of the implementations of Eclipse
MicroProfile , which makes it possible to use multiple APIs, as previously known to Java EE developers (for example, JAX-RS, CDI), and newer ones (Health Check, Metrics, Fault Tolerance etc.). In the variant Helidon SE, the developers were guided by the principle of “No magic”, which is expressed, in particular, in the smaller number or complete absence of annotations necessary to create the application.
Helidon SE is selected for microservice development. Among other things, it lacks the means to implement Dependency Injection, so
Koin was used to implement dependencies. The following is the class containing the main method. To implement Dependency Injection, the class is inherited from
KoinComponent . First, Koin starts, then the required dependencies are initialized and the
startServer()
method is called, where an object of the
WebServer type is created, to which the application configuration and routing setup are passed; after start the application is registered in Consul:
object HelidonServiceApplication : KoinComponent { @JvmStatic fun main(args: Array<String>) { val startTime = System.currentTimeMillis() startKoin { modules(koinModule) } val applicationInfoService: ApplicationInfoService by inject() val consulClient: Consul by inject() val applicationInfoProperties: ApplicationInfoProperties by inject() val serviceName = applicationInfoProperties.name startServer(applicationInfoService, consulClient, serviceName, startTime) } } fun startServer( applicationInfoService: ApplicationInfoService, consulClient: Consul, serviceName: String, startTime: Long ): WebServer { val serverConfig = ServerConfiguration.create(Config.create().get("webserver")) val server: WebServer = WebServer .builder(createRouting(applicationInfoService)) .config(serverConfig) .build() server.start().thenAccept { ws -> val durationInMillis = System.currentTimeMillis() - startTime log.info("Startup completed in $durationInMillis ms. Service running at: http://localhost:" + ws.port())
Routing is configured as follows:
private fun createRouting(applicationInfoService: ApplicationInfoService) = Routing.builder() .register(JacksonSupport.create()) .get("/application-info", Handler { req, res -> val requestTo: String? = req.queryParams() .first("request-to") .orElse(null) res .status(Http.ResponseStatus.create(200)) .send(applicationInfoService.get(requestTo)) }) .get("/application-info/logo", Handler { req, res -> res.headers().contentType(MediaType.create("image", "png")) res .status(Http.ResponseStatus.create(200)) .send(applicationInfoService.getLogo()) }) .error(Exception::class.java) { req, res, ex -> log.error("Exception:", ex) res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send() } .build()
The application uses the config in
HOCON format:
webserver { port: 8081 } application-info { name: "helidon-service" framework { name: "Helidon SE" release-year: 2019 } }
For configuration, it is also possible to use files in the formats JSON, YAML and properties (more
here ).
Ktor service
The framework is written in Kotlin. A new project can be created in several ways: using the build system,
start.ktor.io or the plug-in to IntelliJ IDEA (more
here ).
As in Helidon SE, Ktor is missing the DI “out of the box”, so before starting the server using Koin, dependencies are introduced:
val koinModule = module { single { ApplicationInfoService(get(), get()) } single { ApplicationInfoProperties() } single { MicronautServiceClient(get()) } single { Consul.builder().withUrl("http://localhost:8500").build() } } fun main(args: Array<String>) { startKoin { modules(koinModule) } val server = embeddedServer(Netty, commandLineEnvironment(args)) server.start(wait = true) }
The modules necessary for the application are specified in the configuration file (only the HOCON format is possible; for more information on configuring the Ktor server
here ), the contents of which are presented below:
ktor { deployment { host = localhost port = 8082 watch = [io.heterogeneousmicroservices.ktorservice] } application { modules = [io.heterogeneousmicroservices.ktorservice.module.KtorServiceApplicationModuleKt.module] } } application-info { name: "ktor-service" framework { name: "Ktor" release-year: 2018 }
In Ktor and Koin, the term “module” is used, with different meanings. In Koin, a module is an analogue of the application context in the Spring Framework. The Ktor module is a user-defined function that accepts an object of type
Application and can configure pipeline, set features (
features ), register routes, process
requests and so on:
fun Application.module() { val applicationInfoService: ApplicationInfoService by inject() if (!isTest()) { val consulClient: Consul by inject() registerInConsul(applicationInfoService.get(null).name, consulClient) } install(DefaultHeaders) install(Compression) install(CallLogging) install(ContentNegotiation) { jackson {} } routing { route("application-info") { get { val requestTo: String? = call.parameters["request-to"] call.respond(applicationInfoService.get(requestTo)) } static { resource("/logo", "logo.png") } } } }
This code snippet configures the routing of requests, in particular, the static resource
logo.png
. Ktor-service may contain features. A feature is a functionality that is embedded in the request-response
pipeline (
DefaultHeaders, Compression, and others in the example code above). You can implement your own features, for example, below is the code implementing the Service Discovery pattern in combination with client load balancing based on the Round-robin algorithm:
class ConsulFeature(private val consulClient: Consul) { class Config { lateinit var consulClient: Consul } companion object Feature : HttpClientFeature<Config, ConsulFeature> { var serviceInstanceIndex: Int = 0 override val key = AttributeKey<ConsulFeature>("ConsulFeature") override fun prepare(block: Config.() -> Unit) = ConsulFeature(Config().apply(block).consulClient) override fun install(feature: ConsulFeature, scope: HttpClient) { scope.requestPipeline.intercept(HttpRequestPipeline.Render) { val serviceName = context.url.host val serviceInstances = feature.consulClient.healthClient().getHealthyServiceInstances(serviceName).response val selectedInstance = serviceInstances[serviceInstanceIndex] context.url.apply { host = selectedInstance.service.address port = selectedInstance.service.port } serviceInstanceIndex = (serviceInstanceIndex + 1) % serviceInstances.size } } } }
The main logic is in the
install
method: during the
Render request phase (which runs before the
Send phase), the name of the called service is first determined, then the
consulClient
queried for a list of instances of this service, after which the instance defined using the Round-robin algorithm is called. Thus, the following call becomes possible:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking { httpClient.get<ApplicationInfo>("http://$serviceName/application-info") }
Micronaut service
Micronaut is developed by the creators of the
Grails framework and is inspired by the experience of building services using Spring, Spring Boot and Grails. The framework is a polyglot, supporting the languages ​​of Java, Kotlin and Groovy;
may be Scala support. Dependencies are introduced at compile time, which results in less memory consumption and faster application launch compared to Spring Boot.
The main class has the following form:
object MicronautServiceApplication { @JvmStatic fun main(args: Array<String>) { Micronaut.build() .packages("io.heterogeneousmicroservices.micronautservice") .mainClass(MicronautServiceApplication.javaClass) .start() } }
Some components of the application based on Micronaut are similar to their counterparts in the application on Spring Boot, for example, below is the controller code:
@Controller( value = "/application-info", consumes = [MediaType.APPLICATION_JSON], produces = [MediaType.APPLICATION_JSON] ) class ApplicationInfoController( private val applicationInfoService: ApplicationInfoService ) { @Get fun get(requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo) @Get("/logo", produces = [MediaType.IMAGE_PNG]) fun getLogo(): ByteArray = applicationInfoService.getLogo() }
Kotlin support in Micronaut is based on the
kapt compiler
plugin (more
here ). The assembly script is configured as follows:
plugins { ... kotlin("kapt") ... } dependencies { kapt("io.micronaut:micronaut-inject-java") ... kaptTest("io.micronaut:micronaut-inject-java") ... }
The following shows the contents of the configuration file:
micronaut: application: name: micronaut-service server: port: 8083 consul: client: registration: enabled: true application-info: name: ${micronaut.application.name} framework: name: Micronaut release-year: 2018
Configuring microservice is also possible with JSON, properties and Groovy file formats (more details
here ).
Spring Boot service
The framework was created to simplify the development of applications using the Spring Framework ecosystem. This is achieved through auto-configuration mechanisms when libraries are connected. Below is the controller code:
@RestController @RequestMapping(path = ["application-info"], produces = [MediaType.APPLICATION_JSON_UTF8_VALUE]) class ApplicationInfoController( private val applicationInfoService: ApplicationInfoService ) { @GetMapping fun get(@RequestParam("request-to") requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo) @GetMapping(path = ["/logo"], produces = [MediaType.IMAGE_PNG_VALUE]) fun getLogo(): ByteArray = applicationInfoService.getLogo() }
Microservice is configured with a YAML format file:
spring: application: name: spring-boot-service server: port: 8084 application-info: name: ${spring.application.name} framework: name: Spring Boot release-year: 2014
It is also possible to use properties files for configuration (more
here ).
Launch
The project runs on JDK 12, although, probably, on the 11th version, too, you only need to change the
jvmTarget
parameter in the
jvmTarget
scripts:
withType<KotlinCompile> { kotlinOptions { jvmTarget = "12" ... } }
Before launching microservices, you need to
install Consul and
start an agent - for example, like this:
consul agent -dev
.
Microservice launch is possible from:
After starting all microservices on
http://localhost:8500/ui/dc1/services
you will see:

API Testing
As an example, the results of testing the Helidon service API are given:
GET http://localhost:8081/application-info
{ "name": "helidon-service", "framework": { "name": "Helidon SE", "releaseYear": 2019 }, "requestedService": null }
GET http://localhost:8081/application-info?request-to=ktor-service
{ "name": "helidon-service", "framework": { "name": "Helidon SE", "releaseYear": 2019 }, "requestedService": { "name": "ktor-service", "framework": { "name": "Ktor", "releaseYear": 2018 }, "requestedService": null } }
GET http://localhost:8081/application-info/logo
Returns an image.
You can test an arbitrary microservice API using
Postman (
collection of requests), IntelliJ IDEA
HTTP client (
collection of requests), browser or another tool. In the case of using the first two clients, you must specify the port of the called microservice in the corresponding variable (in Postman it is in the
menu of the collection -> Edit -> Variables , and in the HTTP Client - in the environment variable specified in
this file), and when testing method 2) The API also needs to specify the name of the microservice requested “under the hood”. The answers will be similar to those given above.
Comparison of application parameters
Artifact sizeIn order to preserve the simplicity of setting up and launching applications in assembly scripts, no transitive dependencies were excluded, therefore the size of the Spring Boot uber-JAR service significantly exceeds the size of its counterparts in other frameworks (because when using starters, not only the necessary dependencies are imported; if desired, the size can be significantly reduced):
Start timeThe launch time of each application is intermittent and falls into a certain “window”; The table below shows the start time of the artifact without any additional parameters:
It is worth noting that if you “clean” an application on Spring Boot from unnecessary dependencies and pay attention to setting up the application launch (for example, scan only the necessary packages and use lazy initialization of bins), you can significantly reduce the launch time.
Stress TestingGatling and Scala
script were used for testing. The load generator and the service being tested were run on the same machine (Windows 10, 3.2 GHz quad-core processor, 24 GB RAM, SSD). The port of this service is specified in the Scala script.
For each microservice is determined:
- the minimum amount of heap memory (
-Xmx
) required to run a healthy (responding to) microservice - the minimum amount of heap memory required to pass the load test 50 users * 1000 requests
- the minimum amount of heap memory required to pass the load test 500 users * 1000 requests
Under the passage of the load test is understood that the microservice answered all requests for any time.
It is worth noting that all microservices use the HTTP server Netty.
Conclusion
The task was to create a simple service with the HTTP API and the ability to function in ISA - it was possible to perform on all the frameworks under consideration. It's time to take stock and consider their pros and cons.
HelidounStandard Edition- pros
- application settings
By all measures showed good results; - “No magic”
The framework justified the principle stated by the developers: to create an application, only one abstract was required ( @JvmStatic
- for the Java-Kotlin interop).
- minuses
- micromimography
Some components necessary for industrial development are missing out of the box, for example, dependency injection and Service Discovery implementation.
MicroprofileMicroservice was not implemented on this framework, so I’ll note only a couple of points I know:
- pros
- implementation of Eclipse MicroProfile
In essence, MicroProfile is Java EE optimized for ISA. Thus, firstly, you get access to the whole variety of Java EE API, including that developed specifically for ISA, secondly, you can change the implementation of MicroProfile to any other (Open Liberty, WildFly Swarm, etc.) .
- additionally
- on MicroProfile Starter you can create from scratch a project with the necessary parameters by analogy with similar tools for other frameworks (for example, Spring Initializr ). At the time of publication of the article Helidon implements MicroProfile 1.2, whereas the latest version of the specification is 3.0.
Ktor- pros
- lightness
Allows you to connect only those functions that are directly needed to perform the task; - application settings
Good results in all respects.
- minuses
- “Sharpened” under Kotlin, that is, develop in Java, it seems, is possible, but not necessary;
- microframework (see similar item for Helidon SE).
- additionally
On the one hand, the framework development concept is not part of the two most popular Java development models (Spring-like (Spring Boot / Micronaut) and Java EE / MicroProfile), which can lead to:
- problem finding specialists;
- Increased time to complete tasks compared to Spring Boot due to the need to explicitly configure the required functionality.
On the other hand, dissimilarity to “classic” Spring and Java EE allows you to look at the development process from a different angle, perhaps more consciously.
Micronaut- pros
- Aot
As previously noted, the AOT allows you to reduce the start time and memory consumed by the application compared to its counterpart on the Spring Boot; - Spring-like development model
Programmers with Spring development experience will not take long to master this framework; - application settings
Good results for all parameters; - polyglot
Support at the first-class level citizen languages ​​Java, Kotlin, Groovy; may be Scala support. In my opinion, this can positively affect the growth of the community. By the way, in June 2019 Groovy ranks 14th in the TIOBE programming language popularity rating, soaring from the 60th a year earlier, thus being in the honorable second place among the JVM-languages; - The Micronaut for Spring project allows, among other things, changing the execution environment of an existing Spring Boot application to Micronaut (with restrictions).
Spring boot- pros
- platform maturity and ecosystem
The “every day” framework. For most everyday tasks, there is already a solution in the programming paradigm of Spring, that is, in the way that many programmers are used to. The development is simplified by the concepts of starters and auto configurations; - the presence of a large number of specialists in the labor market, as well as a significant knowledge base (including documentation and answers to Stack Overflow);
- perspective
I think many will agree that in the near future, Spring will remain the leading development framework.
- minuses
- application settings
The application on this framework was not among the leaders, however, some parameters, as noted earlier, can be optimized independently. It is also worth recalling the presence of the Spring Fu project in active development, the use of which makes it possible to reduce these parameters.
You can also highlight common problems associated with new frameworks that Spring Boot lacks:
- less developed ecosystem;
- a small number of specialists with experience with these technologies;
- more time to complete tasks;
- unclear perspectives.
The frameworks considered belong to different weight categories: Helidon SE and Ktor are
microframes , Spring Boot is a full-stack framework, Micronaut, rather, also full-stack; another category is MicroProfile (for example, Helidon MP). Microfrim functionality is limited, which can slow down the execution of tasks; To clarify the possibility of implementing a particular functionality based on a development framework, I recommend that you familiarize yourself with its documentation.
I don’t dare to judge whether this or that framework will “shoot” in the near future, so in my opinion, for now, it’s best to continue to monitor developments using the existing development framework for solving work tasks.
At the same time, as was shown in the article, new frameworks win Spring Boot on the considered parameters of the received applications. If any of these parameters are critically important for one of your microservices, then perhaps you should pay attention to the frameworks that showed the best results. However, we should not forget that Spring Boot, firstly, continues to improve, and secondly, it has a huge ecosystem and a significant number of Java programmers are familiar with it. There are other frameworks that are not covered in this article: Javalin, Quarkus, etc.
You can view the project code on
GitHub . Thank you for attention!
PS: Thank you
artglorin for help in preparing the article.