Skip to content

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:

Terminal window
dotnet new install Cratis.Templates
dotnet new cratis --language Java -n MyStore
Terminal window
cratis new cratis --language java -n MyStore -o MyStore --allow-scripts yes

Cratis.Templates includes the Java template; Cratis.Templates.Java publishes the same template on its own. There is no cratis-java short name.

  • 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 with Cannot find a Java installation on your machine … matching: {languageVersion=17 …}.
  • Docker with the Compose plugin, for the local Chronicle kernel.
  • Node.js ^20.19.0 or >=22.12.0 and 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.

OptionValuesDefault
--DatabaseMongoDB, PostgreSQL, MsSql, SQLiteMongoDB
--packageManageryarn, pnpm, npm, noneyarn
--SkipAiUpdateInstructionstrue, falsefalse

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:

Terminal window
dotnet new cratis --language Java -n MyStore --Database PostgreSQL
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 settings
Terminal window
cd MyStore
docker compose up -d # Chronicle kernel and Aspire dashboard
./gradlew build # compiles and generates the TypeScript proxies
yarn install # skip if the scaffold already installed dependencies
./gradlew bootRun # backend on http://localhost:8080

In a second terminal, start the frontend:

Terminal window
yarn dev

Vite 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 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
@AllowAnonymous
public 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
@AllowAnonymous
public 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.

Terminal window
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.

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:

DependencyVersionWhere
Kotlin2.4.20build.gradle.kts
KSP2.3.12build.gradle.kts
Spring Boot4.1.1build.gradle.kts
Java toolchain17build.gradle.kts
Gradle9.1.0gradle/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.

  • Cannot find a Java installation … languageVersion=17. Install a JDK 17 and point JAVA_HOME at it.
  • Cannot find module during the frontend build. Yarn 2 or later installed with Plug’n’Play because .yarnrc.yml is missing (projects scaffolded from earlier template versions do not have one). Add one containing nodeLinker: node-modules and run yarn install again.
  • Port 8080 or 35000 is already in use. Stop the other process, or move the service. For the backend, set server.port in application.properties and update the two http://localhost:8080 proxy targets in .frontend/vite.config.ts. For Chronicle, change the host side of the mapping in docker-compose.yml and point cratis.chronicle.connection-string at the new port.
  • The backend is reachable from other machines. Spring Boot listens on every interface by default. Set server.address=127.0.0.1 in application.properties to keep it local; the compose ports can be limited the same way by prefixing each mapping with 127.0.0.1:.

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:

Terminal window
cratis ai update

cratis new runs this step for you. See the CLI AI documentation for the full command reference.