Skip to content

Strongly-typed identifiers

A String is a String is a String. Nothing stops a book’s identifier being passed where a member’s was expected — both compile, and the bug surfaces in production as a lookup that quietly finds nothing.

fun borrow(bookId: String, memberId: String) { }
// Compiles. Wrong.
borrow(memberId, bookId)

Give each value a type of its own and the compiler catches it while you are still typing.

A concept is any type implementing ConceptAs<T>:

import io.cratis.chronicle.concepts.ConceptAs
data class BookId(override val value: String) : ConceptAs<String>
data class MemberId(override val value: String) : ConceptAs<String>
data class CopyNumber(override val value: Int) : ConceptAs<Int>

Then use them wherever the value used to be:

@EventType
data class BookBorrowed(
val book: BookId = BookId(""),
val member: MemberId = MemberId("")
)
// Won't compile — which is the entire point.
BookBorrowed(memberId, bookId)

A concept serializes as the value it wraps, not as an object wrapping one:

{ "book": "dune", "member": "ada" }

That is what makes concepts something you can adopt for a property that is already in production. The JSON does not change, the schema the kernel validates against does not change, and every event stored before the concept existed still reads back. The kernel never learns that the value has a type on your side, and does not need to.

The same holds for read models, reducer state, and constraint declarations — everything goes through one serializer, so a concept behaves identically wherever it appears.

The event source id is a String on the wire and always will be. So that your side of the call can still be typed, every client method taking one has an overload taking a ConceptAs<String>:

import io.cratis.chronicle.concepts.append
store.eventLog.append(BookId("dune"), BookBorrowed(BookId("dune"), MemberId("ada")))

These are extension functions in io.cratis.chronicle.concepts, so import the one you need. They cover append, appendMany, hasEventsFor, getTailSequenceNumber, getForEventSourceIdAndEventTypes, redactForEventSource, and getInstanceByKey.

A concept is an ordinary type, so Java declares one as a record:

public record BookId(String value) implements ConceptAs<String> {
@Override public String getValue() { return value; }
}

The extra getValue() is the Kotlin property’s accessor — a Java record’s value() does not satisfy it on its own.

Kotlin’s @JvmInline value class also works and avoids the allocation, but its mangled JVM signatures are awkward from Java. Prefer a data class unless the allocation genuinely matters and Java never touches the type.