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

The article builds five microservices with Helidon SE, Ktor, Micronaut, Quarkus and Spring Boot, integrates Consul for service discovery, provides full source code and configuration, shows how to start each service, benchmarks size, startup time and memory usage, and finally lists the pros and cons of each framework.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Five Java/Kotlin Microservice Frameworks Compared: Helidon, Ktor, Micronaut, Quarkus & Spring Boot

Introduction

This guide shows how to build a heterogeneous microservice architecture (MSA) using five Java/Kotlin frameworks—Helidon SE, Ktor, Micronaut, Quarkus, and Spring Boot. Each service exposes a GET /application-info endpoint (optionally with request-to query) and a /application-info/logo endpoint, registers itself with Consul for service discovery, and can call other services via Consul‑based client‑side load balancing.

Prerequisites

JDK 13

Consul (run in dev mode, e.g. consul agent -dev)

Service implementations

Helidon SE

Helidon SE is a lightweight Java SE library. The service uses Koin for dependency injection, reads configuration from an HOCON file, and registers with Consul after startup.

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.builder(createRouting(applicationInfoService))
        .config(serverConfig)
        .build()
    server.start().thenAccept { ws ->
        val duration = System.currentTimeMillis() - startTime
        log.info("Startup completed in $duration 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") { _, res ->
        res.headers().contentType(MediaType.create("image", "png"))
            .status(Http.ResponseStatus.create(200))
            .send(applicationInfoService.getLogo())
    }
    .error(Exception::class.java) { _, res, ex ->
        log.error("Exception:", ex)
        res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send()
    }
    .build()

HOCON configuration (example):

webserver {
  port: 8081
}

application-info {
  name: "helidon-service"
  framework {
    name: "Helidon SE"
    release-year: 2019
  }
}

Ktor

Ktor is a Kotlin‑first framework. It also uses Koin for DI and registers with Consul.

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

Ktor HOCON configuration (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

Micronaut provides compile‑time dependency injection, resulting in low memory consumption and fast startup.

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

YAML configuration (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

Quarkus targets cloud‑native environments. The service is a JAX‑RS resource.

@Path("/application-info")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class ApplicationInfoResource @Inject constructor(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()
}

Consul registration is performed manually via a CDI bean:

@ApplicationScoped
class ConsulRegistrationBean @Inject constructor(private val consulClient: ConsulClient) {
    fun onStart(@Observes event: StartupEvent) {
        consulClient.register()
    }
}

Spring Boot

Spring Boot relies on auto‑configuration and a large ecosystem.

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

YAML configuration (example):

spring:
  application:
    name: spring-boot-service

server:
  port: 8085

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

Starting the services

After Consul is running, launch each service from the command line:

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

When all services are up, the Consul UI (e.g. http://localhost:8500/ui/dc1/services) shows the registered instances.

API testing

Example response from the Helidon service without a downstream request:

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

When the request-to query parameter is set to ktor-service the response includes the downstream service information:

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

The /logo endpoint returns the PNG image.

Comparison

Program size (MB)

Helidon SE: 17.3 MB

Ktor: 22.4 MB

Micronaut: 17.1 MB

Quarkus: 24.4 MB

Spring Boot: 45.2 MB

Startup time (seconds)

Helidon SE: 2.0 s

Ktor: 1.5 s

Micronaut: 2.8 s

Quarkus: 1.9 s

Spring Boot: 10.7 s

Heap memory (MB) for a healthy service

Helidon SE: 11 MB

Ktor: 13 MB

Micronaut: 17 MB

Quarkus: 13 MB

Spring Boot: 18 MB

Conclusion

All five frameworks can implement a simple HTTP API and participate in a Consul‑based heterogeneous MSA. The choice depends on trade‑offs:

Helidon SE

Pros: Minimal boilerplate, fast startup.

Cons: No built‑in DI or service‑discovery support.

Ktor

Pros: Lightweight, Kotlin‑native, good performance.

Cons: Kotlin‑only, fewer out‑of‑the‑box features.

Micronaut

Pros: Compile‑time DI, low memory, familiar to Spring developers.

Cons: Slightly slower startup than Ktor/Quarkus.

Quarkus

Pros: MicroProfile support, Spring compatibility layer, live reload.

Cons: Larger binary, no main method in current version.

Spring Boot

Pros: Mature ecosystem, extensive documentation, auto‑configuration.

Cons: Largest binary, longest startup.

Source code and build scripts are available at https://github.com/rkudryashov/heterogeneous-microservices

Architecture diagram
Architecture diagram
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.

BackendJavaMicroservicesservice discoveryKotlinframework comparisonConsul
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

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.