Skip to content

Add event sourcing to an Arc slice

You have a complete, typed, live full-stack app from the Arc tutorial — and so far it used a database directly. This page shows the write-side move that puts Chronicle underneath the same CQRS boundary. For information systems, this is the direction we usually recommend: keep the Arc command/query shape, and let Chronicle store the facts.

The reassuring part: adopting it is a write-side change. Your queries, your generated proxies, and your React screens don’t move.

If you drew the database-backed slice as an event model, the model barely changes here. The screen, the command, the AuthorRegistered fact, the read model, and the query all stay put. What changes is where the fact lives — a database row becomes a real, stored event — and that unlocks one genuinely new block: a reactor (frame 06), automation that fires when a fact happens.

UI/A: AuthorsUI/A: AuthorsUI/A: AuthorsC/RM: AuthorsC/RM: AuthorsStream: Authors
AddAuthor
RegisterAuthor



id: uuid, name: string
AuthorRegistered



name: string
Author
Authors
WelcomeNewAuthors

Frames 0105 are unchanged from the intro — the fact at 03 is just stored differently now. Frame 06 is the new part: a reactor, which you couldn’t have before because there were no stored events to react to. Let’s prove it on the very first slice.

Set the two versions of the author slice side by side.

The command stops writing a document and starts recording a fact. Handle() returns the event that happened instead of inserting:

[Command]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
public AuthorRegistered Handle() => new(Name); // returns the fact, doesn't write
}
[EventType]
public record AuthorRegistered(AuthorName Name);

The event is an immutable, past-tense fact. [EventType] carries no name argument — Chronicle uses the type name, AuthorRegistered, as its identity. For Chronicle to use an author’s id as the key of their event stream, give the concept an implicit conversion to EventSourceId:

public record AuthorId(Guid Value) : ConceptAs<Guid>(Value)
{
public static AuthorId New() => new(Guid.NewGuid());
public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString());
}

The read model stops being filled by the command and starts being filled by a projection. Mark it [FromEvent<T>] and Chronicle folds the event onto it — AutoMap matches AuthorRegistered.Name straight onto Author.Name, so you write no update code:

[ReadModel]
[FromEvent<AuthorRegistered>]
public record Author([property: Key] AuthorId Id, AuthorName Name)
{
// The query is UNCHANGED from chapter 1.
public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) =>
collection.Observe();
}

That’s the whole change for this slice. Lined up:

Over a databaseWith Chronicle
What Handle() doeswrites a documentreturns an event
What fills the read modelthe command, directlya projection over the event
What you can readcurrent statecurrent state and full history
The query methodAllAuthorsAllAuthorsidentical
The generated proxies & Reactas builtidentical

The query is the same method, so the generated proxy is the same type, so the screen you wrote in chapter 1 keeps working untouched — it’s still subscribed to the read model, which a projection now keeps current instead of the command:

// the read model is now event-sourced — the query signature is unchanged
public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) =>
collection.Observe();

Now that state changes are recorded as facts, you get things a plain database can’t give you:

  • A free audit trail — every change, in order, forever.

  • Rebuild read models from history — model a brand-new view of the past by replaying the events into it.

  • Reactors — automate a follow-up when a fact happens. A reactor is a class marked IReactor; the method’s first parameter is the event it reacts to:

    public class WelcomeNewAuthors(INotificationService notifications) : IReactor
    {
    public Task AuthorRegistered(AuthorRegistered @event, EventContext context) =>
    notifications.Notify($"Welcome aboard, {@event.Name}!");
    }

Storing current state directly is simpler for bounded CRUD surfaces and adoption steps. Event sourcing earns its keep for information systems because history, auditability, replay, integration, and process insight show up over time. Why Event Sourcing explains why we treat it as the default, and When to use event sourcing names the exceptions.

You also saw why the boundary matters: the read side and the entire frontend came along unchanged.

  • The event-sourcing side, in depth — the Chronicle tutorial builds the same library model one event-sourcing concept at a time.
  • The two together, end to end — the Cratis Stack tour and the full-stack capstone put Arc, Chronicle, and Components together on a real feature.
  • Adopt it incrementallyAdopting Cratis walks through moving an existing Arc app’s write side to Chronicle, one slice at a time.

That’s the library slice — built full-stack and type-safe, with CQRS at the boundary and Chronicle underneath when the slice belongs in the event-sourced model. You have the model; go build your own.