Kotlin Quickstart — from Install to a First Request¶
What you get from this chapter
You can install the packages and run a minimal project where two processes call each other.
The project lives at
framework/languages/java/quickstart/kotlin/. The
code blocks below are read from those files when the site is built. The Java and Kotlin
quickstarts share one Gradle build. Without a location store, two processes name each other's
endpoint directly and exchange one request/reply.
0. Clone the examples repository¶
This chapter's project is quickstart/ in the zlink-kotlin-examples repository; tutorial/,
the program read by the feature guides, and samples/ live beside it.
git clone https://github.com/zlink-systems/zlink-kotlin-examples.git
cd zlink-kotlin-examples/quickstart
main is the latest release plus the fixes merged since, with package versions pinned to that
release. An older release is the tag vA.B.C (git checkout vA.B.C). Send issues and PRs to
zlink-systems/zlink.
1. Installation¶
- JDK 25 or later. The published
zlink-framework-core0.16.0 has class file major 69 (Java 25) - Access to Maven Central
Get it from Maven Central. The minimal combination needed to build one server is the following.
// The contract and runtime
implementation("systems.zlink:zlink-framework-core")
// DI/lifecycle registration
implementation("systems.zlink:zlink-framework-spring-boot-starter")
// Coroutine idiom
implementation("systems.zlink:zlink-framework-kotlin")
Do not list systems.zlink:zlink (the binding) yourself. zlink-framework-core declares the
version it depends on.
[versions]
# 세 zlink-framework-* artifact를 한 번에 고정한다.
zlinkFramework = "0.30.0"
# 아래 셋은 zlink-framework POM이 선언한 값과 맞춘 것이다.
# 충돌 해석에 맡기지 않는다.
springBoot = "3.5.14"
kotlin = "2.3.21"
jackson = "2.17.2"
# systems.zlink:zlink(binding)는 적지 않는다. zlink-framework-core의 POM이 정한다.
[libraries]
zlink-framework-core = { module = "systems.zlink:zlink-framework-core", version.ref = "zlinkFramework" }
zlink-framework-spring-boot-starter = { module = "systems.zlink:zlink-framework-spring-boot-starter", version.ref = "zlinkFramework" }
zlink-framework-kotlin = { module = "systems.zlink:zlink-framework-kotlin", version.ref = "zlinkFramework" }
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springBoot" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springBoot" }
# Kotlin data class를 기본 JSON codec이 다루려면 필요하다. Kotlin Server는 web starter가
# 없어 전이로 들어오지 않는다.
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
# Spring이 @Configuration·@RestController를 CGLIB로 감싸는데 Kotlin class는 기본이 final이다.
# 이 플러그인이 해당 class를 열어 준다.
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
Artifacts to add when you need them:
| Artifact | When to add it |
|---|---|
zlink-framework-locations-redis |
When using the Redis location store for auto-connect (Location) |
zlink-framework-codec-protobuf · -codec-msgpack |
To use instead of the default JSON codec (Handlers and Message Processing) |
zlink-stream-connector |
When building an external client (a game client, mobile) (STREAM) |
zlink-http-client |
When the server calls out over HTTP (HTTP Client guide) |
The license differs by layer — core/binding is MPL-2.0, framework is FSL-1.1-ALv2, and
zlink-http-client is Apache-2.0. There is no cost to building and selling a service.
2. Shared contract¶
package systems.zlink.quickstart.shared
// Request/reply contract shared by the server and client processes.
data class Hello(val name: String)
data class Greeting(val text: String)
3. The handling side¶
addHandlersFromPackageOf only discovers handler types. Which channel exposes one is a
separate registration on channelName(...).server().
This process serves no HTTP, so SpringApplication's isKeepAlive must be set to true;
without it the JVM exits right after the context refreshes.
package systems.zlink.quickstart.server
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.Bean
import systems.zlink.framework.ZLinkMessageContext
import systems.zlink.framework.channels.ZLinkRequestHandler
import systems.zlink.framework.kotlin.addHandlersFromPackageOf
import systems.zlink.framework.kotlin.addRequestHandler
import systems.zlink.framework.spring.EnableZLinkFramework
import systems.zlink.framework.spring.ZLinkFrameworkConfigurer
import systems.zlink.quickstart.shared.Greeting
import systems.zlink.quickstart.shared.Hello
@EnableZLinkFramework
@SpringBootApplication
class ServerApplication {
@Bean
fun zlink(): ZLinkFrameworkConfigurer = ZLinkFrameworkConfigurer { options ->
// Discovers handler types.
options.addHandlersFromPackageOf<ServerApplication>()
// Names the mesh.
val mesh =
options
.addRouteMesh("services")
// This process's own endpoint, for peers to connect to.
.listen("tcp://127.0.0.1:7101")
// This process handles the "greeting" channel.
mesh.channelName("greeting").server().addRequestHandler<HelloHandler, Hello, Greeting>()
}
}
fun main(args: Array<String>) {
// This process serves no HTTP; without setKeepAlive the JVM exits as soon
// as the context finishes refreshing.
val app = SpringApplication(ServerApplication::class.java)
app.isKeepAlive = true
app.run(*args)
}
// Handles one request on the "greeting" channel. ZLinkRequestHandler returns a
// CompletionStage; a Kotlin suspend fun cannot override it. See ../../../README.md.
class HelloHandler : ZLinkRequestHandler<Hello, Greeting> {
override fun handle(request: Hello, context: ZLinkMessageContext): CompletionStage<Greeting> =
CompletableFuture.completedFuture(Greeting("hello, ${request.name}"))
}
4. The calling side¶
package systems.zlink.quickstart.client
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
import systems.zlink.framework.channels.ZLinkRouteClient
import systems.zlink.framework.kotlin.kotlin
import systems.zlink.framework.kotlin.requestToChannel
import systems.zlink.framework.spring.EnableZLinkFramework
import systems.zlink.framework.spring.ZLinkFrameworkConfigurer
import systems.zlink.quickstart.shared.Greeting
import systems.zlink.quickstart.shared.Hello
@EnableZLinkFramework
@SpringBootApplication
class ClientApplication {
@Bean
fun zlink(): ZLinkFrameworkConfigurer = ZLinkFrameworkConfigurer { options ->
// This process also needs its own endpoint.
val mesh = options.addRouteMesh("services").listen("tcp://127.0.0.1:7102")
// This side only calls; it does not handle "greeting".
mesh.channelName("greeting").client()
// Manual connection -- the server's endpoint is given directly.
mesh.peerConnections().connect("tcp://127.0.0.1:7101")
}
}
fun main(args: Array<String>) {
runApplication<ClientApplication>(*args)
}
@RestController
class HelloController(routeClient: ZLinkRouteClient) {
private val route = routeClient.kotlin()
@GetMapping("/hello/{name}")
suspend fun hello(@PathVariable name: String): String =
// The target is a single ChannelName; which node handles it is not specified.
route.requestToChannel<Greeting>("greeting", Hello(name)).await().text
}
5. Run¶
cd zlink-kotlin-examples/quickstart
./gradlew :kotlin:Server:installDist :kotlin:Client:installDist
# Two terminals. Start the server first.
./kotlin/Server/build/install/Server/bin/Server
./kotlin/Client/build/install/Client/bin/Client
curl http://127.0.0.1:5080/hello/world
The response is hello, world with status 200.
6. What to check when the first run fails¶
| Symptom | What to check |
|---|---|
| An artifact is not found | Check that the artifact names in section 1 were copied exactly. The binding version is not pinned separately |
| Startup fails | Check that both processes name the same mesh, and that the listen endpoint does not collide with another process |
| The server exits immediately | Check that isKeepAlive is set to true |
| A call ends with no target | Check that the receiving side registered that channel name in the server role, and that the two processes are connected as peers |
| No answer arrives | Check that the caller used request. A send receives no answer |
7. What to carry over¶
| File | Content |
|---|---|
gradle/libs.versions.toml |
One zlinkFramework entry pins all three artifacts. The binding stays transitive |
| Shared | The message types both processes share |
| Server | addRouteMesh → listen → channelName(...).server().addRequestHandler(...), plus isKeepAlive = true |
| Client | channelName(...).client(), peerConnections().connect(...), requestToChannel(...) |
Further build-setup requirements are listed in the project's README.md.
8. What to read next¶
These two processes connect by writing each other's endpoint directly. Keeping the calling code unchanged while servers are added or restarted at another address needs automatic connection, and that is covered by Location.
- To go over the concepts first — Core Concepts
- The path that calls by name — Channel Messaging
- State objects called by id — Spot · Actor
- To see a complete business flow — Picking a Sample