Comparing Five Java/Kotlin Microservice Frameworks: Helidon, Ktor, Micronaut, Quarkus & Spring Boot

This article reviews five Java/Kotlin microservice frameworks—Helidon, Ktor, Micronaut, Quarkus, and Spring Boot—showing how to build services, configure Consul service discovery, run them, test their APIs, and compare their size, startup time, memory usage, and pros and cons.

Programmer DD
Programmer DD
Programmer DD
Comparing Five Java/Kotlin Microservice Frameworks: Helidon, Ktor, Micronaut, Quarkus & Spring Boot

Preface

Beyond Spring Boot, Java and Kotlin offer many alternatives for building microservices. This article creates five services using different frameworks and connects them via Consul service discovery, forming a heterogeneous microservice architecture (MSA).

Prerequisites

JDK 13

Consul

Creating Applications from Scratch

New projects can be generated with web starters or IDE tools.

Helidon Service

Helidon, originally an Oracle internal project, offers a simple SE version and a MicroProfile (MP) version. Helidon MP implements Eclipse MicroProfile APIs; Helidon SE follows a “no magic” approach with minimal annotations and uses Koin for dependency injection.

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())
    consulClient.agentClient().register(createConsulRegistration(serviceName, ws.port()))
  }
  return server
}

Routing configuration:

private fun createRouting(applicationInfoService: ApplicationInfoService) = Routing.builder()
  .register(JacksonSupport.create())
  .get("/application-info") { req, res ->
    val requestTo = req.queryParams().first("request-to").orElse(null)
    res.status(Http.ResponseStatus.create(200)).send(applicationInfoService.get(requestTo))
  }
  .get("/application-info/logo") { req, res ->
    res.headers().contentType(MediaType.create("image", "png"))
      .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()

Configuration uses HOCON format, but JSON, YAML, or properties are also supported.

Ktor Service

Ktor is a Kotlin‑first framework without built‑in DI; Koin is used for dependency injection before starting the server.

val koinModule = module {
  single { ApplicationInfoService(get(), get()) }
  single { ApplicationInfoProperties() }
  single { ServiceClient(get()) }
  single { Consul.builder().withUrl("https://localhost:8500").build() }
}

fun main(args: Array<String>) {
  startKoin { modules(koinModule) }
  val server = embeddedServer(Netty, commandLineEnvironment(args))
  server.start(wait = true)
}

Configuration (HOCON) example:

ktor {
  deployment {
    host = localhost
    port = 8082
    environment = prod
    autoreload = true
    watch = [io.heterogeneousmicroservices.ktorservice]
  }
  application {
    modules = [io.heterogeneousmicroservices.ktorservice.module.KtorServiceApplicationModuleKt.module]
  }
}

application-info {
  name = "ktor-service"
  framework {
    name = "Ktor"
    release-year = 2018
  }
}

Micronaut Service

Micronaut, created by the Grails author, supports Java, Kotlin, and Groovy with compile‑time dependency injection, resulting in lower memory usage and faster startup compared to Spring Boot.

object MicronautServiceApplication {
  @JvmStatic
  fun main(args: Array<String>) {
    Micronaut.build()
      .packages("io.heterogeneousmicroservices.micronautservice")
      .mainClass(MicronautServiceApplication.javaClass)
      .start()
  }
}

Controller example:

@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()
}

Configuration (YAML) example:

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

Quarkus Service

Quarkus targets cloud‑native environments, offering low memory consumption and fast startup, with built‑in live reload. It implements Eclipse MicroProfile and provides Spring compatibility layers.

@Path("/application-info")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class ApplicationInfoResource(
  @Inject private val applicationInfoService: ApplicationInfoService
) {
  @GET
  fun get(@QueryParam("request-to") requestTo: String?): Response =
    Response.ok(applicationInfoService.get(requestTo)).build()

  @GET
  @Path("/logo")
  @Produces("image/png")
  fun logo(): Response = Response.ok(applicationInfoService.getLogo()).build()
}

Quarkus does not include built‑in service‑discovery for Consul/Eureka; the Consul client is used manually as in other frameworks.

Spring Boot Service

Spring Boot leverages auto‑configuration to simplify development.

@RestController
@RequestMapping(path = ["application-info"], produces = [MediaType.APPLICATION_JSON_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()
}

Configuration (YAML):

spring:
  application:
    name: spring-boot-service

server:
  port: 8085

application-info:
  name: ${spring.application.name}
  framework:
    name: Spring Boot
    release-year: 2014

Starting the Microservices

Install Consul and start its agent (e.g., consul agent -dev). Then launch each service with its respective JAR:

java -jar helidon-service/build/libs/helidon-service-all.jar
java -jar ktor-service/build/libs/ktor-service-all.jar
java -jar micronaut-service/build/libs/micronaut-service-all.jar
java -jar quarkus-service/build/quarkus-service-1.0.0-runner.jar
java -jar spring-boot-service/build/libs/spring-boot-service.jar

After all services are running, open http://localhost:8500/ui/dc1/services to view the registered services.

API Testing

Example responses from the Helidon service:

{
  "name": "helidon-service",
  "framework": {"name": "Helidon SE", "releaseYear": 2019},
  "requestedService": null
}
{
  "name": "helidon-service",
  "framework": {"name": "Helidon SE", "releaseYear": 2019},
  "requestedService": {
    "name": "ktor-service",
    "framework": {"name": "Ktor", "releaseYear": 2018},
    "requestedService": null
  }
}

Tools such as Postman, IntelliJ HTTP client, or a browser can be used for testing.

Comparison of Microservice Frameworks

Key comparison points include program size, startup time, and memory usage. Spring Boot’s uber‑JAR is larger due to starters, while frameworks like Helidon SE produce smaller binaries. Startup times vary; Micronaut’s AOT compilation reduces both startup latency and memory consumption. All services use Netty as the HTTP server.

Conclusion

All examined frameworks can provide a simple HTTP API service within an MSA. Choosing the right one depends on trade‑offs such as startup speed, memory footprint, ecosystem maturity, and available expertise.

Helidon SE

Pros: Minimal annotation required.
Cons: Lacks out‑of‑the‑box DI and service‑discovery integration.

Helidon MP

Pros: Implements Eclipse MicroProfile; can switch to other MicroProfile runtimes.

Ktor

Pros: Lightweight, only adds needed features.
Cons: Kotlin‑only; fewer experts; more manual configuration than Spring Boot.

Micronaut

Pros: AOT reduces startup time and memory; familiar to Spring developers.

Quarkus

Pros: Eclipse MicroProfile implementation; Spring compatibility layer.

Spring Boot

Pros: Mature ecosystem, extensive documentation, many experts.
Cons: Larger binaries and more configuration parameters; can be optimized with Spring Fu.
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaMicroservicesservice discoveryKotlinframework comparisonConsul
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.