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.
Frames 01–05 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.
What changes, and what doesn’t
Section titled “What changes, and what doesn’t”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 database | With Chronicle | |
|---|---|---|
What Handle() does | writes a document | returns an event |
| What fills the read model | the command, directly | a projection over the event |
| What you can read | current state | current state and full history |
| The query method | AllAuthors | AllAuthors — identical |
| The generated proxies & React | as built | identical |
The frontend doesn’t notice
Section titled “The frontend doesn’t notice”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 unchangedpublic static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) => collection.Observe();// not one character changesconst [authors] = AllAuthors.use();What the events unlock
Section titled “What the events unlock”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}!");}
Why this is usually the better backbone
Section titled “Why this is usually the better backbone”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 incrementally — Adopting 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.