Build your first Arc application with Kotlin
Prerequisites
Section titled “Prerequisites”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.
Compiler compatibility
Section titled “Compiler compatibility”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.
Configure Gradle
Section titled “Configure Gradle”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=2Add the application model
Section titled “Add the application model”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.Commandimport io.cratis.arc.artifacts.FromServicesimport io.cratis.arc.artifacts.ReadModelimport io.cratis.arc.authorization.AllowAnonymousimport io.cratis.arc.queries.Pathimport java.util.UUIDimport java.util.concurrent.ConcurrentHashMapimport org.springframework.stereotype.Repository
@Repositorypublic 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@AllowAnonymouspublic 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@AllowAnonymouspublic 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.SpringBootApplicationimport org.springframework.boot.runApplication
@SpringBootApplicationclass TaskApplication
fun main(args: Array<String>) { runApplication<TaskApplication>(*args)}Run the application:
./gradlew bootRunExecute the command
Section titled “Execute the command”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"}}Query the read model
Section titled “Query the read model”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.