Outbox and Inbox
The outbox/inbox pattern is a well-established way to integrate distributed systems reliably. Chronicle formalizes this pattern as first-class, kernel-managed event sequences.
The Outbox
Section titled “The Outbox”Every event store in Chronicle has a well-known outbox sequence (EventSequenceId.Outbox). Events appended to the outbox are visible to any other event store that has set up a subscription.
The outbox exists by convention — you do not need to create it. Append events to it the same way you append to any other event sequence.
The Inbox
Section titled “The Inbox”When event store A subscribes to event store B, Chronicle automatically manages an inbox sequence on A for events forwarded from B. The well-known inbox sequence is EventSequenceId.Inbox, and the per-source inbox identifier is derived from the source event store name using EventSequenceId.InboxPrefix:
using Cratis.Chronicle.EventSequences;
public static class SubscriptionsOutboxInboxId{ public static EventSequenceId Resolve() { var inboxId = new EventSequenceId($"{EventSequenceId.InboxPrefix}source-event-store"); // Resolves to: "inbox-source-event-store" return inboxId; }}EventSequenceId.InboxPrefix is a small C#-only convenience constant — the other clients don’t have a dedicated helper for it, since the inbox sequence id itself ("inbox-<source-event-store>") is just a plain string wherever event sequence ids are used.
You never write to an inbox directly. Chronicle forwards events from the source outbox to the target inbox as they are appended.
Lifecycle
Section titled “Lifecycle”Subscriptions are kernel-managed and persistent. Once registered, a subscription:
- Survives client disconnections
- Is restarted automatically when the Kernel starts
- Is tracked per event store, not per client session
This means you can call Subscribe at application startup without worrying about duplicate registration — if the subscription already exists with the same identifier, no new event is appended.
Event Flow
Section titled “Event Flow”Source event store Target event store───────────────── ───────────────── Outbox Inbox ┌─────────┐ [subscription] ┌─────────┐ │ event A │ ─────────────────▶ │ event A │ │ event B │ ─────────────────▶ │ event B │ └─────────┘ └─────────┘The EventStoreSubscriptionObserverSubscriber grain on the Kernel side observes the source outbox and appends forwarded events to the corresponding target inbox.
Reactors, Projections, and Reducers on the Inbox
Section titled “Reactors, Projections, and Reducers on the Inbox”Client-side observers (reactors, projections, reducers) can target EventSequenceId.Inbox to process all events arriving from subscribed sources. The client SDK automatically routes inbox-targeted observers to the correct per-source inbox sequence.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Reactors;
[EventType]public record SubscriptionsOutboxInboxOrderPlaced(Guid OrderId);
public class SubscriptionsOutboxInboxIncomingOrdersReactor : IReactor{ public Task OrderPlaced(SubscriptionsOutboxInboxOrderPlaced @event, EventContext context) { // Handles OrderPlaced events from any subscribed source event store return ProcessAsync(@event.OrderId); }
Task ProcessAsync(Guid orderId) => Task.CompletedTask;}import io.cratis.chronicle.events.EventContextimport io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reactor
@EventType(id = "subscriptions-outbox-inbox-order-placed")data class SubscriptionsOutboxInboxOrderPlaced(val orderId: String)
@Reactorclass SubscriptionsOutboxInboxIncomingOrdersReactor { fun orderPlaced(event: SubscriptionsOutboxInboxOrderPlaced, context: EventContext) { // Handles OrderPlaced events from any subscribed source event store process(event.orderId) }
private fun process(orderId: String) {}}import io.cratis.chronicle.events.EventContext;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reactor;
@EventType(id = "subscriptions-outbox-inbox-order-placed")record SubscriptionsOutboxInboxOrderPlaced(String orderId) {}
@Reactorclass SubscriptionsOutboxInboxIncomingOrdersReactor { void orderPlaced(SubscriptionsOutboxInboxOrderPlaced event, EventContext context) { // Handles OrderPlaced events from any subscribed source event store process(event.orderId()); }
private void process(String orderId) {}}defmodule MyApp.Events.SubscriptionsOutboxInboxOrderPlaced do use Chronicle.Events.EventType, id: "subscriptions-outbox-inbox-order-placed"
defstruct [:order_id]end
defmodule MyApp.Reactors.SubscriptionsOutboxInboxIncomingOrdersReactor do use Chronicle.Reactors.Reactor
alias MyApp.Events.SubscriptionsOutboxInboxOrderPlaced
@handles SubscriptionsOutboxInboxOrderPlaced
@impl true def handle(%SubscriptionsOutboxInboxOrderPlaced{}, _context) do # Handles OrderPlaced events from any subscribed source event store :ok endendimport { eventType, reactor } from '@cratis/chronicle';
@eventType()class SubscriptionsOutboxInboxOrderPlaced { constructor(readonly orderId: string) {}}
@reactor()class SubscriptionsOutboxInboxIncomingOrdersReactor { // Method name must be the exact camelCase of the event's class name - // Chronicle discovers handlers by name, not by parameter type. async subscriptionsOutboxInboxOrderPlaced(event: SubscriptionsOutboxInboxOrderPlaced): Promise<void> { // Handles OrderPlaced events from any subscribed source event store await this.process(event.orderId); }
private async process(orderId: string): Promise<void> {}}