Skip to content

Event sourcing in Kotlin and Java

Chronicle is an open-source (MIT) event-sourcing database and processing runtime, and io.cratis:chronicle is its idiomatic JVM client — one artifact, usable from both Kotlin and Java. Events are data classes or records annotated with @EventType, read models are classes annotated with @ReadModel, and reducers, reactors, projections, and constraints follow the same pattern. Artifacts on your classpath are found and registered with the kernel when you connect, so there is no schema file or registry to keep in sync. Kotlin gets suspending calls throughout; Java gets blocking bridges for the same surface. A Spring Boot starter reduces setup to a dependency, and chronicle-testing provides an in-process test library.

With event sourcing, your application appends immutable facts, and every view of the world is derived from them rather than overwritten on top of them. Chronicle stores those events and enforces rules — uniqueness and other constraints are checked at append time, on the server. Nothing is updated in place, so every state your system has been in is reconstructible.

The event store is not JVM-only: Chronicle’s kernel exposes a language-agnostic gRPC/protobuf boundary, and the same store is reachable from the .NET, TypeScript, and Elixir clients as well. Storage is pluggable — MongoDB by default, with PostgreSQL, SQL Server, SQLite, and in-memory providers implemented in the kernel.

Replace <version> with the current release from Maven Central:

build.gradle.kts
dependencies {
implementation("io.cratis:chronicle:<version>")
// Or, in a Spring Boot application:
implementation("io.cratis:chronicle-spring-boot-starter:<version>")
}

An annotated event, the read model it produces, and the reducer that folds one into the other — from the client repository’s example:

@EventType
data class EmployeeHired(
val firstName: String = "",
val lastName: String = "",
val title: String = ""
)
@ReadModel
data class EmployeeState(
val id: String = "",
val firstName: String = "",
val title: String = ""
)
@Reducer
class EmployeeStateReducer {
fun employeeHired(event: EmployeeHired) =
EmployeeState(
firstName = event.firstName,
title = event.title)
}

Connect, wait for the first registration pass, and append an event. Registration does not mean the reducer has processed the append: read models update asynchronously. This example keeps the client alive while it waits up to ten seconds for the model, then fails on timeout rather than printing a missing result. It assumes a Chronicle kernel running locally in development mode; Get started with Chronicle shows how to start one.

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
fun main() = runBlocking {
val client = ChronicleClient(ChronicleOptions.development())
try {
val store = client.getEventStore("MyApp")
store.awaitRegistration()
store.eventLog.append("employee-1", EmployeeHired("Ada", "Lovelace", "Engineer"))
val ada = withTimeout(10_000) {
var state = store.readModels.getInstanceByKey(EmployeeState::class, "employee-1")
while (state == null) {
delay(100)
state = store.readModels.getInstanceByKey(EmployeeState::class, "employee-1")
}
state
}
println(ada)
} finally {
client.dispose()
}
}

Use a fresh event store for this exercise so employee-1 has no older read model. The ten-second limit is a demo timeout, not a processing-time guarantee. If it expires, inspect reducer registration and failed partitions instead of treating the append as a completed projection.

Chronicle imports are omitted; the Kotlin/Java client docs show the full setup, including the Spring Boot starter, which connects for you.

The same surface is available from Java, with events as records and blocking bridges in place of suspending calls.

Looking for another language? See Chronicle in your language for the .NET, TypeScript, and Elixir clients.