The Cratis Kotlin Application template
The Kotlin flavor of the cratis template gives you the full-stack starting point on Spring Boot: Arc for commands and queries over HTTP, Chronicle for the event log, and a React frontend that talks to TypeScript proxies generated from your Kotlin code.
Scaffold it with either tool:
dotnet new install Cratis.Templatesdotnet new cratis --language Kotlin -n MyStorecratis new cratis --language kotlin -n MyStore -o MyStore --allow-scripts yesCratis.Templates includes the Kotlin template; Cratis.Templates.Kotlin publishes the same template on its own. There is no cratis-kotlin short name.
Before you begin
Section titled “Before you begin”- JDK 17. The build uses a Gradle Java toolchain pinned to 17 and does not download one, so a JDK 17 must be installed where Gradle can find it, for example through
JAVA_HOME. A newer JDK on its own fails withCannot find a Java installation on your machine … matching: {languageVersion=17 …}. - Docker with the Compose plugin, for the local Chronicle kernel.
- Node.js
^20.19.0or>=22.12.0and yarn, pnpm, or npm for the frontend.
The .NET SDK is needed only if you scaffold with dotnet new; the generated project builds with the bundled Gradle wrapper.
Options
Section titled “Options”| Option | Values | Default |
|---|---|---|
--Database | MongoDB, PostgreSQL, MsSql, SQLite | MongoDB |
--packageManager | yarn, pnpm, npm, none | yarn |
--SkipAiUpdateInstructions | true, false | false |
The application code is the same for every database, because the Chronicle kernel persists the read models. --Database changes how the Chronicle container in docker-compose.yml stores its data (embedded MongoDB, a PostgreSQL or SQL Server container, or an SQLite file on a named volume) and switches the default read-model sink in ChronicleConfiguration.kt between MongoDB and SQL:
dotnet new cratis --language Kotlin -n MyStore --Database PostgreSQLWhat you get
Section titled “What you get”MyStore/├── build.gradle.kts — Gradle build with the Arc plugin├── settings.gradle.kts — plugin and dependency repositories├── gradlew, gradle/wrapper/ — Gradle wrapper (9.1.0)├── run.sh — starts Chronicle, the backend, and the frontend together├── docker-compose.yml — Chronicle dev container + Aspire dashboard├── package.json — frontend tooling (Vite, React, Cratis Components)├── .frontend/ — Vite and TypeScript configuration├── App.tsx, Home.tsx — frontend application shell├── SomeModule/ — the sample frontend slices└── src/main/ ├── kotlin/com/example/ │ ├── MyStoreApplication.kt — Spring Boot entry point │ ├── ChronicleConfiguration.kt — Chronicle options and domain artifact registration │ └── somemodule/ — the sample backend vertical slices └── resources/application.properties — event store name and route settingsRun it
Section titled “Run it”cd MyStoredocker compose up -d # Chronicle kernel and Aspire dashboard./gradlew build # compiles and generates the TypeScript proxiesyarn install # skip if the scaffold already installed dependencies./gradlew bootRun # backend on http://localhost:8080In a second terminal, start the frontend:
yarn devVite serves the frontend on http://localhost:9000, opens it in your browser, and forwards /api and /.cratis to the backend. Open Demo and register an item; it appears in the list.
./run.sh runs the same sequence in one terminal When it exits, whether you press Ctrl+C or a step fails, it runs docker compose down, which removes the containers and, with the default MongoDB storage, every event and read model. Use the manual steps with docker compose stop when you want to keep data between runs. It uses yarn when available and falls back to npm; ./run.sh --no-frontend starts only Chronicle and the backend.
The project includes a .yarnrc.yml that sets nodeLinker: node-modules, so Yarn 2 and later install into node_modules instead of Plug’n’Play, which the TypeScript 7 compiler cannot resolve. npm, pnpm, and Yarn 1 ignore the file.
The sample slices teach the conventions
Section titled “The sample slices teach the conventions”The Registration slice shows the command side: a @Command with a @CommandKey, returning the event to append. This excerpt from registration/Registration.kt omits the Registered event and the reactor declared in the same file:
@Command@AllowAnonymousdata class Register( @CommandKey val id: SomeId, val name: SomeName) { fun handle(): Registered = Registered(name)}SomeId is a strongly-typed identity, a ConceptAs<String>, rather than a raw String. The command key routes the command to its event source; the generated TypeScript command sends it as part of the payload.
The Listing slice shows the read side: a read model built by a @Reducer from the Registered event, with a query exposing it over HTTP. This excerpt from listing/Listing.kt omits the reducer:
@ArcReadModel@ChronicleReadModel@AllowAnonymousdata class Listing( val id: String = "", val name: SomeName = SomeName("")) { companion object { @JvmStatic @Path("/api/listings") suspend fun all( @FromServices eventStore: IEventStore ): List<Listing> = eventStore.readModels.getInstances(Listing::class) }}With the backend running, post to the command route and the whole loop runs: the command is handled, Chronicle appends the event, the reactor logs it, and the reducer updates the read model.
curl -sS -X POST http://localhost:8080/api/register \ -H 'Content-Type: application/json' \ -d '{"id":"'$(uuidgen)'","name":"Cratis"}'
curl -sS -X QUERY http://localhost:8080/api/listings \ -H 'Content-Type: application/json' \ -d '{"arguments":{}}'The first call returns "isSuccess":true. The second returns the listing in data, for example [{"id":"…","name":"Cratis"}]. GET /api/listings returns the same result.
Versions
Section titled “Versions”The Arc Gradle plugin (io.cratis.arc) and io.cratis:cratis, the one dependency that bundles Arc, its Spring Boot wiring, and the Chronicle client, are requested as latest.release. Gradle resolves them when it builds, and the Chronicle client version follows from the Arc release, so the two stay paired without your involvement. Gradle caches a dynamic version for up to 24 hours by default; ./gradlew build --refresh-dependencies picks up a newer release immediately.
For a reproducible or production build, replace latest.release with the exact version you tested, keeping the plugin and the io.cratis:cratis dependency on the same version, or enable Gradle dependency locking.
The rest of the build is pinned:
| Dependency | Version | Where |
|---|---|---|
| Kotlin | 2.4.20 | build.gradle.kts |
| KSP | 2.3.12 | build.gradle.kts |
| Spring Boot | 4.1.1 | build.gradle.kts |
| Java toolchain | 17 | build.gradle.kts |
| Gradle | 9.1.0 | gradle/wrapper/gradle-wrapper.properties |
build.gradle.kts also pins kotlinx-coroutines-core to the version the Cratis libraries are compiled against, because Spring Boot’s dependency management would otherwise select an older one that fails at startup.
Troubleshooting
Section titled “Troubleshooting”Cannot find a Java installation … languageVersion=17. Install a JDK 17 and pointJAVA_HOMEat it.Cannot find moduleduring the frontend build. Yarn 2 or later installed with Plug’n’Play because.yarnrc.ymlis missing (projects scaffolded from earlier template versions do not have one). Add one containingnodeLinker: node-modulesand runyarn installagain.- Port 8080 or 35000 is already in use. Stop the other process, or move the service. For the backend, set
server.portinapplication.propertiesand update the twohttp://localhost:8080proxy targets in.frontend/vite.config.ts. For Chronicle, change the host side of the mapping indocker-compose.ymland pointcratis.chronicle.connection-stringat the new port. - The backend is reachable from other machines. Spring Boot listens on every interface by default. Set
server.address=127.0.0.1inapplication.propertiesto keep it local; the compose ports can be limited the same way by prefixing each mapping with127.0.0.1:.
AI assistance
Section titled “AI assistance”The scaffolded project includes a .cratis/ai.json selecting the cratis/application/kotlin profile with the Kotlin and TypeScript languages. After dotnet new, make sure the Cratis CLI is installed, then run:
cratis ai updatecratis new runs this step for you. See the CLI AI documentation for the full command reference.