๐Ÿ“œ Chronicle for the JVM

August 28, 2026 ยท View on GitHub

๐Ÿ“œ Chronicle for the JVM

Event sourcing for Kotlin and Java โ€” write the facts, annotate them, and the rest of the system builds itself.

Discord Maven Central Build Publish License


A chronicle is a record of what happened, in the order it happened โ€” kept so that the story can be told again, from the beginning, to anyone who asks. Cratis Chronicle is that idea as a database: your application appends facts, and every view of the world is derived from them rather than overwritten on top of them.

This repository is the JVM client for it. One artifact, io.cratis:chronicle, idiomatic from both Kotlin and Java, plus a Spring Boot starter that reduces setup to a dependency.

Behind it sits a conviction: almost any system that deals with information and business flows is better told this way โ€” and telling it should feel like writing ordinary Kotlin or Java, familiar even if you have never event-sourced before. The whole Cratis ecosystem is designed around that: deliberately simple, light on ceremony, built with productivity, quality, and reliability in mind โ€” and AI-friendly by design, with free AI skills for building with it.

โœจ Why you might want this

  • You declare facts, not plumbing. An event is a data class or a record with @EventType on it. A read model is a class with @ReadModel. There is no schema file, no registry, no builder to keep in sync.
  • Everything registers itself. Every artifact on your classpath is found and registered with the kernel the moment you connect โ€” in the order the kernel needs them. Manual registration is still there when you want it, one call away.
  • The past is never lost. Nothing is updated in place. Every state your system has ever been in is reconstructible, which turns "how did this record end up like that?" from an archaeology project into a query.
  • Coroutines all the way down. Every call that touches the kernel suspends. Java gets blocking bridges for the same surface, so neither language is the second-class one.
  • Rules the kernel enforces. Uniqueness and other constraints are checked at append time, on the server, not by a read-then-write race in your code.

๐Ÿ“œ What a slice looks like

Four annotated types, and you have an event-sourced feature โ€” the fact, the state it produces, the fold that produces it, and the rule that guards it:

KotlinJava
@EventType
data class EmployeeHired(
    val firstName: String = "",
    val lastName: String = "",
    val title: String = ""
)

@EventType
data class EmployeePromoted(val newTitle: 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)

    fun employeePromoted(
        event: EmployeePromoted,
        state: EmployeeState?
    ) = (state ?: EmployeeState())
        .copy(title = event.newTitle)
}
@EventType
public record EmployeeHired(
    String firstName,
    String lastName,
    String title
) {}

@EventType
public record EmployeePromoted(String newTitle) {}

@ReadModel
public class EmployeeState {
    private String id = "";
    private String firstName = "";
    private String title = "";
    // getters and setters
}

@Reducer
public class EmployeeStateReducer {
    public EmployeeState employeeHired(
            EmployeeHired event) {
        return new EmployeeState("",
            event.firstName(), event.title());
    }

    public EmployeeState employeePromoted(
            EmployeePromoted event,
            EmployeeState state) {
        var current = state != null
            ? state : new EmployeeState();
        current.setTitle(event.newTitle());
        return current;
    }
}

No registration code appears anywhere above, and none is needed. Append an event and the read model is there:

store.eventLog.append("employee-1", EmployeeHired("Ada", "Lovelace", "Engineer"))
val ada = store.readModels.getInstanceByKey(EmployeeState::class, "employee-1")

๐Ÿงฉ The cast

Everything you write is one of a handful of kinds. Annotate it, and the client finds it:

ArtifactYou writeIt does
Event type@EventType on a data class or recordRecords a fact that happened. Immutable, forever
Read model@ReadModel on a classThe shape some part of your system reads
Reducer@Reducer with one method per eventFolds a stream of events into a read model
ProjectionIProjectionFor<T>, or @FromEvent on the modelDeclares the same fold, without writing the fold
Reactor@Reactor with one method per eventDoes something when a fact arrives โ€” mail, calls, more events
ConstraintIConstraint with @ConstraintA rule the kernel enforces before an append is accepted
SeederICanSeedEvents with @SeederFacts that should exist the first time the system runs
MigrationIEventTypeMigration<New, Old>Carries an event type forward to a new generation
WebhookIWebhookDefinerPushes events out to something beyond your process

๐ŸŽฅ How it fits together

flowchart LR
    App["โœ๏ธ your code<br/>appends a fact"] -->|"@EventType"| Log[["๐Ÿ“œ event log<br/>the record of what happened"]]
    Discovery["๐Ÿ” discovery<br/>scans your classpath"] -.->|"registers everything"| Kernel
    Log --> Kernel(["โš™๏ธ Chronicle kernel"])
    Kernel --> Reducers["๐Ÿ” reducers<br/>+ projections"]
    Kernel --> Reactors["โšก reactors"]
    Kernel --> Constraints["๐Ÿ›ก๏ธ constraints<br/>checked on append"]
    Reducers --> ReadModels["๐Ÿ“Š read models<br/>what your API returns"]
    Reactors --> Effects["๐Ÿ“ฎ side effects<br/>mail ยท calls ยท new events"]

The one arrow worth pointing at is the dotted one. You never build that list โ€” the client does, on every connect, so a kernel that restarts is told everything again without you noticing.

๐Ÿš€ Quick start

You need a kernel. The development image is a single command:

docker run -p 35000:35000 cratis/chronicle:latest-development

Plain Kotlin or Java

// build.gradle.kts
dependencies {
    implementation("io.cratis:chronicle:2.1.1")
}
// build.gradle
dependencies {
    implementation 'io.cratis:chronicle:2.1.1'
}

Connect, wait for the first registration pass, and go:

KotlinJava
fun main() = runBlocking {
    val client = ChronicleClient(
        ChronicleOptions.development())
    val store = client.getEventStore("MyApp")
    store.awaitRegistration()

    store.eventLog.append(
        "employee-1",
        EmployeeHired("Ada", "Lovelace", "Engineer"))

    val ada = store.readModels
        .getInstanceByKey(
            EmployeeState::class, "employee-1")
    println(ada)

    client.dispose()
}
public static void main(String[] args) {
    var client = new ChronicleClient(
        ChronicleOptions.Companion.development());
    var store = client.getEventStore(
        "MyApp", "Default");
    EventStoreJavaBridge.awaitRegistration(store);

    EventLogJavaBridge.append(
        store.getEventLog(), "employee-1",
        new EmployeeHired(
            "Ada", "Lovelace", "Engineer"), null);

    var ada = ReadModelsJavaBridge
        .getInstanceByKey(store.getReadModels(),
            EmployeeState.class, "employee-1");
    System.out.println(ada);

    client.dispose();
}

Java cannot call Kotlin suspend functions, so the client ships a blocking bridge per service in io.cratis.chronicle.java โ€” same surface, no coroutines required.

Spring Boot

The starter brings the client with it and wires everything up:

// build.gradle.kts
dependencies {
    implementation("io.cratis:chronicle-spring-boot-starter:2.1.1")
}
cratis:
  chronicle:
    event-store: Ordering

That is the whole setup. Your artifacts are discovered in your application's packages and registered before the first request is served, and an IEventStore is ready to inject:

KotlinJava
@RestController
class Employees(
    private val eventStore: IEventStore
) {
    @PostMapping("/employees/{id}/hire")
    fun hire(
        @PathVariable id: String,
        @RequestBody hire: Hire
    ) = runBlocking {
        eventStore.eventLog.append(id,
            EmployeeHired(
                hire.firstName,
                hire.lastName,
                hire.title))
    }
}
@RestController
public class Employees {
    private final Chronicle chronicle;

    public Employees(Chronicle chronicle) {
        this.chronicle = chronicle;
    }

    @PostMapping("/employees/{id}/hire")
    public void hire(
            @PathVariable String id,
            @RequestBody Hire hire) {
        chronicle.append(id,
            new EmployeeHired(
                hire.firstName(),
                hire.lastName(),
                hire.title()));
    }
}

Artifacts are activated through the Spring container, so a reactor takes its dependencies through its constructor like any @Service. On top of that the starter gives every request an identity, a causation trail, and a unit of work โ€” and can route each one to its own tenant namespace from a header, a subdomain, or a claim. See the Spring Boot guide.

๐ŸŒ Facts with a place

A fact often happened somewhere. Point, LineString and Polygon are ordinary properties on an event, read model or reducer state, and they serialize as GeoJSON โ€” which is how the kernel recognizes the value as geospatial and how the sink knows to index and query it:

KotlinJava
@EventType
data class WarehouseInspected(
    val warehouseId: String = "",
    val inspectedAt: Point = Point(0.0, 0.0)
)

store.eventLog.append(
    "warehouse-1",
    WarehouseInspected("warehouse-1", Point(10.75, 59.91)))
@EventType
public record WarehouseInspected(
    String warehouseId,
    Point inspectedAt
) {}

store.getEventLog().append(
    "warehouse-1",
    new WarehouseInspected(
        "warehouse-1", new Point(10.75, 59.91)));

On the wire that becomes the shape the kernel looks for, with no mapping of your own:

{ "warehouseId": "warehouse-1", "inspectedAt": { "type": "Point", "coordinates": [10.75, 59.91] } }

See Geospatial types for the full reference, including polygons with holes.

๐Ÿงฐ What's in this repo

PieceWhat it isWhere
io.cratis:chronicleThe JVM client โ€” annotations, coroutines, artifact discovery, Java bridgesSource
io.cratis:chronicle-spring-boot-starterSpring Boot auto-configuration, multi-tenancy, per-request identity and units of workIntegrations/SpringBoot
io.cratis:chronicle-testingIn-process scenarios for specifying a slice with no kernel, container or databaseTesting
Kotlin console sampleAn interactive tour of the whole API, with registration done by handSamples/Kotlin/Console
Java console sampleThe same tour in JavaSamples/Java/Console
Kotlin Spring Boot sampleAn event-sourced HTTP API with no setup code at allSamples/Kotlin/SpringBoot
Java Spring Boot sampleThe same application in JavaSamples/Java/SpringBoot
DocumentationGetting started, guides, and the API referenceDocumentation

๐Ÿ—บ๏ธ Start here

  • Get Started โ€” install, connect, append, and read a read model back, in Kotlin and Java. Start here.
  • Artifact Registration โ€” what gets discovered, in what order, and how to narrow, replace or turn it off.
  • Spring Boot โ€” the starter, multi-tenancy, and per-request identity, causation and units of work.
  • Reference โ€” every annotation, the IEventStore API, and the full configuration surface.
  • Cratis Chronicle โ€” the kernel this client talks to, and the concepts behind it.

โ–ถ๏ธ Running the samples

docker run -p 35000:35000 cratis/chronicle:latest-development

gradle :Samples:Kotlin:Console:run          # interactive tour, Kotlin
gradle :Samples:Java:Console:run            # interactive tour, Java
gradle :Samples:Kotlin:SpringBoot:bootRun   # HTTP API on :8080
gradle :Samples:Java:SpringBoot:bootRun     # HTTP API on :8081

โœ… Quality gates

gradle build                                # every module builds clean
gradle test                                 # all specs pass

cd Documentation && ./verify-markdown.sh    # docs lint + every link resolves
python3 Documentation/validate-client-snippets.py   # every doc snippet compiles

Documentation snippets are not decorative โ€” every Kotlin and Java fence in Documentation/ is compiled against the real client on every build, so an example that references an API that no longer exists fails CI rather than a reader.

๐Ÿงฉ The Cratis ecosystem

This project is part of Cratis โ€” free, MIT-licensed tools for building event-sourced and CQRS applications.

  • Chronicle โ€” event-sourcing database and runtime. Orleans-based kernel, pluggable storage (MongoDB default; PostgreSQL, SQL Server, SQLite, in-memory), language-agnostic gRPC contracts. Docs
  • Chronicle clients โ€” first-class .NET SDK, plus TypeScript, Kotlin/Java (this repository), and Elixir; Python coming soon (pre-alpha). AI agents connect through the Chronicle MCP server.
  • Arc โ€” opinionated CQRS framework for ASP.NET Core with commands, queries, validation, authorization, and TypeScript proxy generation. Works without event sourcing. Docs
  • Components โ€” React components aligned with Arc patterns. Docs
  • CLI + Workbench โ€” inspect and diagnose Chronicle from the terminal or the browser. Docs
  • Model-first layer (experimental) โ€” Studio, Screenplay, Stage, Scene, Prologue
  • Supporting โ€” Fundamentals, Specifications, Synopsis, Lens, Narrator, and free AI tooling (preview); Ensemble coming soon (pre-release)
  • Samples โ€” runnable event sourcing and CQRS samples for the whole stack

Everything Cratis publishes today is MIT licensed and free to use.


Part of the Cratis platform ยท Licensed under the MIT license