1. Your first event
Every library starts the same way: a book shows up. So that’s where we’ll start too — by recording that fact. By the end of this chapter you’ll have written your first event to Chronicle and understood why we reach for an event instead of a database row.
Think in facts, not rows
Section titled “Think in facts, not rows”Your instinct, coming from most databases, is probably to create a Book row and INSERT it. Hold that thought — because it throws away the most interesting thing: that the book arrived, and when. In event sourcing we record what happened. The book arriving is a fact, and facts have a few properties: they’re immutable (it happened; you can’t un-happen it), and they’re named in the past tense.
So let’s name it. A BookAdded event, as a record marked with [EventType]:
using Cratis.Chronicle.Events;
[EventType]public record BookAdded(string Title, string Isbn);import io.cratis.chronicle.events.EventType
@EventType(id = "BookAdded")data class BookAdded(val title: String, val isbn: String)import io.cratis.chronicle.events.EventType;
@EventType(id = "BookAdded")record BookAdded(String title, String isbn) {}defmodule MyApp.Events.BookAdded do use Chronicle.Events.EventType, id: "book-added"
defstruct [:title, :isbn]endimport { eventType } from '@cratis/chronicle';
@eventType()class BookAdded { constructor( readonly title: string, readonly isbn: string ) {}}A couple of things worth noticing here:
[EventType]carries no name — Chronicle uses the type’s name (BookAdded) as the event’s identity. That’s a small thing now, but it’s why you’ll never hand-maintain a string registry of event names.- The properties aren’t nullable. An event states what was true the moment it happened. If you ever find yourself reaching for a nullable property, that’s Chronicle nudging you: you probably have a second fact hiding in there, and it deserves its own event.
Give the book an identity
Section titled “Give the book an identity”Every event is about something — here, a specific book. Chronicle calls that the event source, and the events that share a source form that book’s own little stream of history. In C#, we’ll identify a book with a strongly-typed id rather than a bare Guid, so the compiler stops us from ever mixing a book’s id up with, say, a member’s:
using Cratis.Chronicle.Events;
public record BookId(Guid Value) : EventSourceId<Guid>(Value){ public static BookId New() => new(Guid.NewGuid());}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.By inheriting from EventSourceId<Guid>, BookId becomes a specialized concept — a ConceptAs<> targeting EventSourceId — so it converts to and from the stream key automatically: you hand a BookId straight to Chronicle, no ceremony. And because the type itself says “I am an event source id”, conventions elsewhere in the stack pick it up — Arc, for instance, recognizes an EventSourceId-derived parameter on a command and resolves which stream to write to without any wiring from you. One small record, and the whole stack knows what a book’s identity is. The event source concept goes deeper on how these per-thing streams of facts work.
Kotlin, Java, Elixir, and TypeScript don’t currently have an equivalent typed-id wrapper — event source ids in those clients are plain strings, so the compiler can’t stop you from mixing up a book’s id and a member’s the way it can in C#.
Append it
Section titled “Append it”Now the moment itself. With a ChronicleClient connected to your event store, append the event against the book’s id (in C#, the strongly-typed BookId; in the other clients, a plain string you generate yourself):
using Cratis.Chronicle;
public static class TutorialFirstEventAppend{ public static async Task<BookId> AddBook(IEventStore eventStore) { var book = BookId.New(); await eventStore.EventLog.Append(book, new BookAdded("The Pragmatic Programmer", "978-0135957059")); return book; }}import io.cratis.chronicle.IEventStoreimport java.util.UUID
class TutorialFirstEventAppend { suspend fun addBook(eventStore: IEventStore): String { val bookId = UUID.randomUUID().toString() eventStore.eventLog.append(bookId, BookAdded("The Pragmatic Programmer", "978-0135957059")) return bookId }}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.eventSequences.AppendResult;import kotlinx.coroutines.BuildersKt;import kotlin.coroutines.EmptyCoroutineContext;import kotlin.coroutines.Continuation;import java.util.UUID;
class TutorialFirstEventAppend { String addBook(IEventStore eventStore) throws InterruptedException { var bookId = UUID.randomUUID().toString();
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, (scope, continuation) -> { @SuppressWarnings("unchecked") var appendContinuation = (Continuation<? super AppendResult>) continuation; return eventStore.getEventLog().append( bookId, new BookAdded("The Pragmatic Programmer", "978-0135957059"), null, appendContinuation); });
return bookId; }}defmodule MyApp.TutorialFirstEventAppend do alias MyApp.Events.BookAdded
def add_book do book_id = Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)
:ok = Chronicle.append(book_id, %BookAdded{ title: "The Pragmatic Programmer", isbn: "978-0135957059" })
book_id endendimport { Guid, IEventStore } from '@cratis/chronicle';
class TutorialFirstEventAppend { async addBook(eventStore: IEventStore): Promise<string> { const bookId = Guid.create().toString(); await eventStore.eventLog.append(bookId, new BookAdded('The Pragmatic Programmer', '978-0135957059')); return bookId; }}Run it. Nothing dramatic appears on screen — and that’s exactly right. Behind that one line, Chronicle validated the event against its registered schema, assigned it the next sequence number, and committed it to the event log — permanently, and in order. The fact is now part of your system’s history; nothing will ever quietly overwrite it.
What you did
Section titled “What you did”- Modeled a real-world moment as an immutable event (
BookAdded) instead of a row to be updated later. - Gave the book a strongly-typed event source id so its events form one stream.
- Appended that event to the log — your first permanent fact.
A fact you can’t query, though, isn’t much use yet — right now the book exists only as history. In the next chapter we’ll fix that: we’ll teach Chronicle to fold this stream of events into a Books read model you can actually query, and watch it update itself as more events arrive. Onward →