Skip to content

Build your first Arc application with Kotlin

Use JDK 17 and Gradle 8.14.4. This tutorial follows the passing Samples/Kotlin/SpringBoot application in the repository. The local workspace version is 0.0.0-SNAPSHOT; substitute a released version when consuming published artifacts.

For Java records and CompletionStage, follow Build your first Arc application with Java.

Use Kotlin 2.4.10 and KSP 2.3.11 (KSP2), as in the examples below. Arc’s compiled classes carry Kotlin metadata version 2.4.0; Kotlin 2.1.0 and 2.2.0 reject that metadata when compiling a consumer. Upgrade the consumer compiler rather than disabling metadata validation, and retain the Kotlin runtime versions selected by the dependency graph. JVM bytecode still targets Java 17.

Compiler-generated forwarding methods for inherited interface defaults may appear in implementation classes’ getDeclaredMethods() results. Reflective lookup can return a class-declared forwarder instead of an inherited interface method, changing its declaring class and Method.isDefault() result. An added declared method does not necessarily represent a new handwritten implementation or a changed default.

The repository requires Gradle 8.14.4 and JDK 17. Use the checked-in wrapper with Kotlin 2.4.10 and KSP 2.3.11; this baseline avoids the Kotlin plugin’s deprecated Gradle version warning without suppressing it.

The Arc plugin marker is configured for publication through Maven Central. Add Maven Central to plugin resolution in settings.gradle.kts:

pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}

The preferred setup is the Arc plugin. It applies Kotlin/JVM and KSP, adds io.cratis:arc and io.cratis:arc-ksp, targets JDK 17, and treats warnings as errors. Create build.gradle.kts with the following content. Its repositories block resolves application and processor dependencies; pluginManagement.repositories alone does not resolve them.

plugins {
id("io.cratis.arc") version "<version>"
kotlin("plugin.spring") version "2.4.10"
id("org.springframework.boot") version "4.1.1"
id("io.spring.dependency-management") version "1.1.7"
}
repositories {
mavenCentral()
}
cratisArc {
moduleName.set("TaskApplication")
dependencyVersion.set("<version>")
endpoints {
segmentsToSkip.set(2)
}
}
dependencies {
implementation("io.cratis:arc-spring-boot-starter:<version>")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
}

If the plugin is not available in your build, use manual KSP setup:

plugins {
kotlin("jvm") version "2.4.10"
kotlin("plugin.spring") version "2.4.10"
id("com.google.devtools.ksp") version "2.3.11"
id("org.springframework.boot") version "4.1.1"
id("io.spring.dependency-management") version "1.1.7"
}
dependencies {
implementation("io.cratis:arc:<version>")
implementation("io.cratis:arc-spring-boot-starter:<version>")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
ksp("io.cratis:arc-ksp:<version>")
}
repositories {
mavenCentral()
}
ksp {
arg("arc.moduleName", "TaskApplication")
}

Set the matching host route convention in src/main/resources/application.properties. The example package has two segments, so skipping both produces /api/create-task:

cratis.arc.endpoints.segments-to-skip-for-route=2

Create src/main/kotlin/example/tasks/Tasks.kt with a Spring repository, a command, and a read model. Arc discovers the model and resolves parameters from Spring. Use public model types, properties, and invocation methods. Kotlin’s default public visibility is supported; the explicit public keywords below are style, not a compiler requirement. @AllowAnonymous makes the tutorial endpoints callable without authentication.

package example.tasks
import io.cratis.arc.artifacts.Command
import io.cratis.arc.artifacts.FromServices
import io.cratis.arc.artifacts.ReadModel
import io.cratis.arc.authorization.AllowAnonymous
import io.cratis.arc.queries.Path
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import org.springframework.stereotype.Repository
@Repository
public class TaskRepository {
private val tasks = ConcurrentHashMap<String, TaskView>()
public fun create(title: String): TaskView =
TaskView(UUID.randomUUID().toString(), title.trim()).also { tasks[it.id] = it }
public fun all(): List<TaskView> = tasks.values.sortedBy(TaskView::title)
}
public data class TaskCreated(public val id: String, public val title: String)
@Command
@AllowAnonymous
public data class CreateTask(public val title: String) {
public fun handle(repository: TaskRepository): TaskCreated {
val task = repository.create(title)
return TaskCreated(task.id, task.title)
}
}
@ReadModel
@AllowAnonymous
public data class TaskView(public val id: String, public val title: String) {
public companion object {
@JvmStatic
@Path("/api/tasks")
public fun all(@FromServices repository: TaskRepository): List<TaskView> = repository.all()
}
}

Create src/main/kotlin/example/TaskApplication.kt so Spring scans the example.tasks package:

package example
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class TaskApplication
fun main(args: Array<String>) {
runApplication<TaskApplication>(*args)
}

Run the application:

Terminal window
./gradlew bootRun
Terminal window
curl -sS -X POST http://localhost:8080/api/create-task \
-H 'Content-Type: application/json' \
-d '{"title":"Try Arc"}'

The identifier changes on every run. The response has this shape:

{"correlationId":"<uuid>","isAuthorized":true,"validationResults":[],"exceptionMessages":[],"exceptionStackTrace":"","authorizationFailureReason":"","isValid":true,"hasExceptions":false,"isSuccess":true,"response":{"id":"<task-id>","title":"Try Arc"}}
Terminal window
curl -sS -X QUERY http://localhost:8080/api/tasks \
-H 'Content-Type: application/json' \
-d '{"arguments":{}}'

The one-shot query returns the created task in a QueryResult envelope:

{"correlationId":"<uuid>","data":[{"id":"<task-id>","title":"Try Arc"}],"isReady":true,"isAuthorized":true,"validationResults":[],"exceptionMessages":[],"exceptionStackTrace":"","paging":{"page":0,"size":0,"totalItems":1,"totalPages":0},"isValid":true,"hasExceptions":false,"isSuccess":true}

The repository’s :GradlePlugin:test --tests '*ArcOnboardingFunctionalTest' check materializes the preferred Kotlin and Java tutorial files, resolves locally staged Arc plugin-marker and runtime publications, compiles generated artifacts, and sends these POST and QUERY requests to a real Spring Boot host on a random port. It substitutes only the Arc version and local Arc repository seams, with an added test probe; public transitive dependencies still use the documented repositories. This is separate from the source-only documentation snippet checker and does not compile every documentation snippet or execute the manual setup.

Continue with the commands guide and queries guide.