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.
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: 2018Quarkus
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: 2014Starting 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.jarWhen 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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
