---
title: Build your first Arc application with Kotlin
description: Create and run a minimal Kotlin Spring Boot command and query with Arc's generated, model-bound endpoints.
---


## 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](/arc/backend/kotlin/get-started/java/).

### 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

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

```kotlin
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.

```kotlin
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:

```kotlin
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`:

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

## 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.

```kotlin
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:

```kotlin
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:

```bash
./gradlew bootRun
```

## Execute the command

```bash
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:

```json
{"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

```bash
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:

```json
{"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](/arc/backend/kotlin/guides/commands/) and [queries guide](/arc/backend/kotlin/guides/queries/).
