---
title: 2. Building a read model
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/tutorial/read-model.mdx
description: Turn the library's stream of events into a queryable Books read model — declaratively, with no update code.
---

import { Tabs, TabItem } from '@astrojs/starlight/components';


We can record that a book arrived, but the librarian can't *see* the catalog yet — the books live only as history in the log. In this chapter we'll fix that: we'll build a `Books` read model that always reflects the current state of every book, and — here's the part that surprises people coming from CRUD — we'll do it **without writing a single line that updates anything**.

In [event-modeling](/event-modeling/) terms that's the **view pattern** — events fold into a read model the UI can query. It's the slice of the model we build in this chapter:

```mermaid
eventmodeling

tf 01 event Library.BookAdded
tf 02 event Library.BookBorrowed
tf 03 event Library.BookReturned
tf 04 readmodel Library.Book ->> 01 ->> 02 ->> 03
```

## First, a couple more facts

A book doesn't just arrive; it gets borrowed and brought back. Those are facts too, so they're events:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;

[EventType]
public record BookBorrowed(string MemberName);

[EventType]
public record BookReturned;
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/tutorial/read-model/a-events.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.events.EventType

@EventType
data class BookBorrowed(val memberName: String)

@EventType
class BookReturned
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/tutorial/read-model/a-events.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.events.EventType;

@EventType
record BookBorrowed(String memberName) {}

@EventType
record BookReturned() {}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/tutorial/read-model/a-events.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Events.BookBorrowed do
  use Chronicle.Events.EventType, id: "book-borrowed"

  defstruct [:member_name]
end

defmodule MyApp.Events.BookReturned do
  use Chronicle.Events.EventType, id: "book-returned"

  defstruct []
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/tutorial/read-model/a-events.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { eventType } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';

@eventType()
class BookBorrowed {
    @field(String) memberName: string;
    constructor(memberName: string) { this.memberName = memberName; }
}

@eventType()
class BookReturned {
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/tutorial/read-model/a-events.md)

</TabItem>
</Tabs>

Notice `BookReturned` has no data at all — and that's fine. The fact that it *happened*, on a particular book's stream, at a particular time, is the whole story. Not every event needs a payload.

## Declare what you want to read

Here's the shift. In a database you'd write code to keep a `Books` table in sync — insert on add, update a flag on borrow, update it back on return. In Chronicle you instead **declare the shape you want** and tell it which events feed it. Chronicle does the keeping-in-sync for you. That declaration is a [projection](/chronicle/concepts/projection/):

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Projections.ModelBound;

[FromEvent<BookAdded>]
public record Book(
    BookId Id,

    string Title,
    string Isbn,

    [SetValue<BookAdded>(false)]
    [SetValue<BookBorrowed>(true)]
    [SetValue<BookReturned>(false)]
    bool OnLoan,

    [SetFrom<BookBorrowed>(nameof(BookBorrowed.MemberName))]
    string? BorrowedBy);
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/tutorial/read-model/book.md)

</TabItem>
<TabItem label="Kotlin">

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/tutorial/read-model/book.md)

</TabItem>
<TabItem label="Java">

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/tutorial/read-model/book.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.ReadModels.Book do
  use Chronicle.ReadModels.ReadModel

  defstruct id: "", title: "", isbn: "", on_loan: false, borrowed_by: ""

  from MyApp.Events.BookAdded,
    set: [id: :event_source_id, title: :title, isbn: :isbn, on_loan: false]

  from MyApp.Events.BookBorrowed,
    set: [on_loan: true, borrowed_by: :member_name]

  from MyApp.Events.BookReturned,
    set: [on_loan: false]
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/tutorial/read-model/book.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { fromEvent, Guid, setFrom, setValue } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';

@fromEvent(BookAdded)
export class Book {
    id: Guid = Guid.empty;

    @setFrom(BookAdded, 'title')
    title = '';

    @setFrom(BookAdded, 'isbn')
    isbn = '';

    @setValue(BookAdded, false)
    @setValue(BookBorrowed, true)
    @setValue(BookReturned, false)
    onLoan = false;

    @setFrom(BookBorrowed, 'memberName')
    @field(String) borrowedBy: string | null = null;
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/tutorial/read-model/book.md)

</TabItem>
</Tabs>

Read the attributes as a sentence: a book is *made from* `BookAdded` — its `Title` and `Isbn` come straight off the event; `OnLoan` is `false` when the book is added, `true` when it's borrowed, and `false` again when it's returned; `BorrowedBy` is set to whoever borrowed it. You're *declaring* how each fact maps onto the view — not writing imperative updates, not worrying about ordering. Chronicle replays the events in order and applies your mapping.

:::note[FromEvent maps by convention]
The class-level `[FromEvent<BookAdded>]` maps event properties to read model properties **by naming convention** — `BookAdded.Title` lands in `Book.Title` and `BookAdded.Isbn` in `Book.Isbn` because the names match, with nothing else to write. You only reach for a property-level `[SetFrom<T>]` when the names *don't* line up — like `BookBorrowed.MemberName` feeding `BorrowedBy` above.
:::

:::tip[Reach for the declarative path first]
For mappings like this — "events map onto fields" — the model-bound attributes (`[FromEvent<T>]` for by-convention mapping, `[SetFrom<T>]` and `[SetValue<T>]` for the exceptions) express it with almost no code. Only when a view genuinely needs hand-written, imperative folding should you drop to a [reducer](/chronicle/reducers/). Try the projection first; you'll rarely need more.
:::

Kotlin and Java's model-bound projections don't currently have an equivalent to `[SetValue<T>]` — setting a literal value per event type — so this particular projection isn't available in those two clients yet.

## Query it

By default Chronicle **materializes** the projection into its configured **sink** storage — MongoDB unless you change it — so the `Book` read model is just a collection you query, exactly what you're used to. It lives in a collection named after the read model, pluralized (`Books`), in the database named after your event store; [the MongoDB setup in Get started](/chronicle/get-started/console/#configure-the-mongodb-client) shows how to point the driver at it:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using MongoDB.Driver;

public class Books(IMongoCollection<Book> collection)
{
    public IEnumerable<Book> OnLoan() => collection.Find(b => b.OnLoan).ToList();
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/tutorial/read-model/query.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.IEventStore
import io.cratis.chronicle.readModels.ReadModel

@ReadModel
data class TutorialBook(val title: String = "", val onLoan: Boolean = false)

/**
 * Kotlin has no raw MongoCollection to query directly - reads go through the read model service
 * instead, which returns already-deserialized instances to filter in memory.
 */
class Books(private val store: IEventStore) {
    suspend fun onLoan(): List<TutorialBook> =
        store.readModels.getInstances(TutorialBook::class).filter { it.onLoan }
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/tutorial/read-model/query.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.IEventStore;
import io.cratis.chronicle.java.ReadModelsJavaBridge;
import io.cratis.chronicle.readModels.ReadModel;

import java.util.List;
import java.util.stream.Collectors;

@ReadModel
record TutorialBook(String title, boolean onLoan) {}

// Java has no raw MongoCollection to query directly - reads go through the read model service
// instead, which returns already-deserialized instances to filter in memory.
class Books {
    private final IEventStore store;

    Books(IEventStore store) {
        this.store = store;
    }

    List<TutorialBook> onLoan() {
        return ReadModelsJavaBridge.getInstances(store.getReadModels(), TutorialBook.class).stream()
            .filter(TutorialBook::onLoan)
            .collect(Collectors.toList());
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/tutorial/read-model/query.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.TutorialQueryBooksService do
  alias MyApp.ReadModels.Book

  def on_loan do
    with {:ok, books} <- Chronicle.all(Book) do
      {:ok, Enum.filter(books, & &1.on_loan)}
    end
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/tutorial/read-model/query.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { IEventStore } from '@cratis/chronicle';

class Books {
    constructor(private readonly store: IEventStore) {}

    async onLoan(): Promise<Book[]> {
        const books = await this.store.readModels.getInstances(Book);
        return books.filter(book => book.onLoan);
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/tutorial/read-model/query.md)

</TabItem>
</Tabs>

Now exercise it. Append a `BookBorrowed` for your book and query again — `OnLoan` is `true`, and `BorrowedBy` has the member's name. Append a `BookReturned` and it flips back. You never wrote an `UPDATE`. The projection did it, by re-deriving the book from its events.

:::note[A heartbeat of delay]
The read model updates *just after* the event is appended, not in the same instant — it's [eventually consistent](/chronicle/read-models/). For a UI that's usually imperceptible (and you can subscribe to live updates). It only matters when you need to make a decision based on current state — which is what constraints and the next chapter's tools are for.
:::

## A view that removes itself

The catalog answers "what books do we have?" — but the librarian's most common question at the desk is sharper: *what's out on loan right now?* You could filter `Book` on `OnLoan`, and we just did. But there's a more direct way to model it: a read model whose instances **exist only while the book is out**. A `BorrowedBook` appears when a book is borrowed, and disappears when it comes back:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Projections.ModelBound;

[FromEvent<BookBorrowed>]
[RemovedWith<BookReturned>]
public record BorrowedBook(
    BookId Id,

    string MemberName);
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/tutorial/read-model/borrowed-book.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.projections.FromEvent
import io.cratis.chronicle.projections.RemovedWith
import io.cratis.chronicle.readModels.ReadModel

@ReadModel
@FromEvent(BookBorrowed::class)
@RemovedWith(BookReturned::class)
data class BorrowedBook(
    val id: String = "",
    val memberName: String = ""
)
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/tutorial/read-model/borrowed-book.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.projections.FromEvent;
import io.cratis.chronicle.projections.RemovedWith;
import io.cratis.chronicle.readModels.ReadModel;

@ReadModel
@FromEvent(eventType = BookBorrowed.class)
@RemovedWith(eventType = BookReturned.class)
class BorrowedBook {
    private String id = "";
    private String memberName = "";

    public BorrowedBook() {}

    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getMemberName() { return memberName; }
    public void setMemberName(String memberName) { this.memberName = memberName; }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/tutorial/read-model/borrowed-book.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.ReadModels.BorrowedBook do
  use Chronicle.ReadModels.ReadModel

  defstruct id: "", member_name: ""

  from MyApp.Events.BookBorrowed,
    set: [id: :event_source_id, member_name: :member_name]

  removed_with MyApp.Events.BookReturned, []
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/tutorial/read-model/borrowed-book.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { fromEvent, Guid, removedWith, setFrom } from '@cratis/chronicle';

@fromEvent(BookBorrowed)
@removedWith(BookReturned)
export class BorrowedBook {
    id: Guid = Guid.empty;

    @setFrom(BookBorrowed, 'memberName')
    memberName = '';
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/tutorial/read-model/borrowed-book.md)

</TabItem>
</Tabs>

Kotlin and Java don't currently have an equivalent to `[RemovedWith<T>]` either, so this one is C#/Elixir/TypeScript only for now.

And its own query, just as plain as the last one:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using MongoDB.Driver;

public class BorrowedBooks(IMongoCollection<BorrowedBook> collection)
{
    public IEnumerable<BorrowedBook> All() => collection.Find(_ => true).ToList();
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/tutorial/read-model/borrowed-books-query.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.IEventStore
import io.cratis.chronicle.readModels.ReadModel

@ReadModel
data class TutorialBorrowedBook(val title: String = "", val memberName: String = "")

/**
 * Kotlin has no raw MongoCollection to query directly - reads go through the read model service
 * instead.
 */
class BorrowedBooks(private val store: IEventStore) {
    suspend fun all(): List<TutorialBorrowedBook> = store.readModels.getInstances(TutorialBorrowedBook::class)
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/tutorial/read-model/borrowed-books-query.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.IEventStore;
import io.cratis.chronicle.java.ReadModelsJavaBridge;
import io.cratis.chronicle.readModels.ReadModel;

import java.util.List;

@ReadModel
record TutorialBorrowedBook(String title, String memberName) {}

// Java has no raw MongoCollection to query directly - reads go through the read model service
// instead.
class BorrowedBooks {
    private final IEventStore store;

    BorrowedBooks(IEventStore store) {
        this.store = store;
    }

    List<TutorialBorrowedBook> all() {
        return ReadModelsJavaBridge.getInstances(store.getReadModels(), TutorialBorrowedBook.class);
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/tutorial/read-model/borrowed-books-query.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.TutorialBorrowedBooksService do
  alias MyApp.ReadModels.BorrowedBook

  def all do
    Chronicle.all(BorrowedBook)
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/tutorial/read-model/borrowed-books-query.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { IEventStore } from '@cratis/chronicle';

class BorrowedBooks {
    constructor(private readonly store: IEventStore) {}

    all(): Promise<BorrowedBook[]> {
        return this.store.readModels.getInstances(BorrowedBook);
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/tutorial/read-model/borrowed-books-query.md)

</TabItem>
</Tabs>

Two attributes carry the whole lifecycle. `[FromEvent<BookBorrowed>]` creates the instance when the borrow happens — `MemberName` mapped by convention, keyed by the book's id. `[RemovedWith<BookReturned>]` is the new move: when a `BookReturned` arrives on that same book's stream, Chronicle **deletes the instance from the sink**. No `IsActive` flag, no soft-delete column, no cleanup job — the collection *is* the answer to "what's out right now", because instances that no longer apply simply aren't in it.

Notice what just happened to your modeling instincts, too: instead of bending one `Books` table to answer every question, you built a second, purpose-shaped view over the *same events*. Read models are cheap in Chronicle — they're derived, so you can have as many as you have questions.

## What you did

- Added the events that make up a book's life (`BookBorrowed`, `BookReturned`).
- **Declared** a `Books` read model and how events map onto it — no update code anywhere.
- **Queried** it like ordinary data, and watched it stay correct on its own.
- Built a second view, `BorrowedBook`, whose instances are **removed** when the book comes back — two questions, two read models, one stream of facts.

You can now see the catalog. The last piece is to make the library *do* something when the world changes — when a book comes back, tell the next person waiting for it. That's a job for a reactor, and it's the [final chapter](/chronicle/tutorial/reacting/). [Let's finish the tour →](/chronicle/tutorial/reacting/)
