The Cratis Java Application template
The Java 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 Java records. The build still applies the Kotlin and KSP Gradle plugins, because Arc generates Kotlin adapters for your Java declarations; the application code you write is Java.
Scaffold it with either tool:
dotnet new install Cratis.Templatesdotnet new cratis --language Java -n MyStorecratis new cratis --language java -n MyStore -o MyStore --allow-scripts yesCratis.Templates includes the Java template; Cratis.Templates.Java publishes the same template on its own. There is no cratis-java 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.java between MongoDB and SQL:
dotnet new cratis --language Java -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/ ├── java/com/example/ │ ├── MyStoreApplication.java — Spring Boot entry point │ ├── ChronicleConfiguration.java — Chronicle options and artifact registration │ └── somemodule/ — the sample backend vertical slices ├── kotlin/com/example/PackageMarker.kt — keeps the package visible to KSP; leave it in place └── 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 record with a @CommandKey, returning the event to append through Arc’s asynchronous Java command path. From registration/Register.java; the Registered event and the reactor have their own files in the same folder:
@Command@AllowAnonymouspublic record Register(@CommandKey SomeId id, SomeName name) { public CompletionStage<Registered> handle() { return CompletableFuture.completedFuture(new Registered(name())); }}SomeId is a strongly-typed identity, a record implementing 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 record built by a @Reducer (in listing/ListingReducer.java) from the Registered event, with a query exposing it over HTTP. From listing/Listing.java:
@io.cratis.arc.artifacts.ReadModel@io.cratis.chronicle.readModels.ReadModel@AllowAnonymouspublic record Listing(String id, SomeName name) { @Path("/api/listings") public static CompletionStage<List<Listing>> all(@FromServices Chronicle chronicle) { return CompletableFuture.completedFuture(chronicle.readModels(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/java profile with the Java 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.