Skip to content

Build your first Arc application with Kotlin

By the end of this page you have a Spring Boot application that accepts a CreateTask command at POST /api/create-task and answers a TaskView query at /api/tasks. You write no controller: Arc generates both endpoints from the model at compile time.

Use JDK 17 and Gradle 8.14.4. This tutorial follows the passing Samples/Kotlin/SpringBoot application in the repository.

Arc’s libraries and Gradle plugin implementation are published to Maven Central under the io.cratis group; the io.cratis.arc plugin marker is io.cratis.arc:io.cratis.arc.gradle.plugin. Replace every <version> below with the same Arc release, for example the latest version listed for io.cratis:arc on Maven Central. Inside this repository the local workspace version is 0.0.0-SNAPSHOT, which is not published.

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

Use Kotlin 2.4.20 and KSP 2.3.12 (KSP2), as in the examples below. The Arc plugin applies exactly this pair itself; choose it yourself only for the manual setup. 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.20 and KSP 2.3.12; this baseline avoids the Kotlin plugin’s deprecated Gradle version warning without suppressing it.

Start in an empty project directory. The Arc plugin marker is published to Maven Central, so add Maven Central to plugin resolution in settings.gradle.kts:

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

Gradle only runs in a directory that contains a build definition, so create the wrapper now, with an installed Gradle, before build.gradle.kts. Every later ./gradlew command then runs Gradle 8.14.4:

Terminal window
gradle wrapper --gradle-version 8.14.4

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.20"
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.20"
kotlin("plugin.spring") version "2.4.20"
id("com.google.devtools.ksp") version "2.3.12"
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

The build runs KSP before compiling, so a model mistake surfaces as an ARCKSP compiler error rather than at startup; the diagnostics reference explains each code. When Spring Boot logs Tomcat started on port 8080, the generated endpoints are live.

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.

If a request returns 404, the route convention and the package disagree; troubleshooting walks through that and other first-run failures.

Continue with the commands guide and queries guide, or generate a typed frontend client with TypeScript proxies.