๐ 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.
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
@EventTypeon 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:
| Kotlin | Java |
|---|---|
|
|
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:
| Artifact | You write | It does |
|---|---|---|
| Event type | @EventType on a data class or record | Records a fact that happened. Immutable, forever |
| Read model | @ReadModel on a class | The shape some part of your system reads |
| Reducer | @Reducer with one method per event | Folds a stream of events into a read model |
| Projection | IProjectionFor<T>, or @FromEvent on the model | Declares the same fold, without writing the fold |
| Reactor | @Reactor with one method per event | Does something when a fact arrives โ mail, calls, more events |
| Constraint | IConstraint with @Constraint | A rule the kernel enforces before an append is accepted |
| Seeder | ICanSeedEvents with @Seeder | Facts that should exist the first time the system runs |
| Migration | IEventTypeMigration<New, Old> | Carries an event type forward to a new generation |
| Webhook | IWebhookDefiner | Pushes 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:
| Kotlin | Java |
|---|---|
|
|
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:
| Kotlin | Java |
|---|---|
|
|
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:
| Kotlin | Java |
|---|---|
|
|
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
| Piece | What it is | Where |
|---|---|---|
io.cratis:chronicle | The JVM client โ annotations, coroutines, artifact discovery, Java bridges | Source |
io.cratis:chronicle-spring-boot-starter | Spring Boot auto-configuration, multi-tenancy, per-request identity and units of work | Integrations/SpringBoot |
io.cratis:chronicle-testing | In-process scenarios for specifying a slice with no kernel, container or database | Testing |
| Kotlin console sample | An interactive tour of the whole API, with registration done by hand | Samples/Kotlin/Console |
| Java console sample | The same tour in Java | Samples/Java/Console |
| Kotlin Spring Boot sample | An event-sourced HTTP API with no setup code at all | Samples/Kotlin/SpringBoot |
| Java Spring Boot sample | The same application in Java | Samples/Java/SpringBoot |
| Documentation | Getting started, guides, and the API reference | Documentation |
๐บ๏ธ 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
IEventStoreAPI, 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