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.
Declaring one
Section titled “Declaring one”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:
@EventTypedata class BookBorrowed( val book: BookId = BookId(""), val member: MemberId = MemberId(""))
// Won't compile — which is the entire point.BookBorrowed(memberId, bookId)What goes on the wire
Section titled “What goes on the wire”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.
Event source ids
Section titled “Event source ids”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.
From Java
Section titled “From Java”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.
Data class or value class
Section titled “Data class or value class”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.