Skip to content

Event sources and streams

An event source definition is a C# type that names a kind of event source, lists the event streams it owns and declares which dimensions its appends are guarded by. You use it to stop passing routing strings around and to get a consistent concurrency guard for every append.

Without a definition, every append and every observer repeats strings such as eventSourceType: "account" and eventStreamType: "Transactions". A typo routes events to a stream nobody reads, and the concurrency scope is something each caller builds by hand. With a definition, the names live in one place, the client checks them before it sends anything, the kernel checks them again, and an append without an explicit scope gets a guard that matches what you declared.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.EventSources;
[EventType]
public record MoneyDeposited(decimal Amount);
[EventSource("Account", Description = "A customer's bank account", Concurrency = ConcurrencyDimensions.EventSourceId)]
[EventStream("Onboarding", Description = "Opening the account and verifying the customer")]
[EventStream(
"Transactions",
Description = "Deposits and withdrawals, one stream per month",
Concurrency = ConcurrencyDimensions.EventSourceId | ConcurrencyDimensions.EventStreamType | ConcurrencyDimensions.EventStreamId)]
public class AccountEventSource : IEventSource;
public static class Deposits
{
public static Task<AppendResult> Deposit(IEventLog eventLog, EventSourceId accountId, decimal amount, string month) =>
eventLog.Append<AccountEventSource>(
accountId,
new MoneyDeposited(amount),
eventStream: "Transactions",
eventStreamId: month);
}

View C# snippet source on GitHub

The definition is AccountEventSource. It is never instantiated: it carries attributes only. The deposit goes to the Transactions stream of one account, with the month as the stream id. Because Transactions declares the event source id, the stream type and the stream id as its dimensions, two deposits to the same account in the same month conflict, and a deposit to another month does not.

Chronicle has long stored four values on every event. A definition gives three of them a declared home.

TermWhat it isWhere it comes from
EventSourceIdThe instance, such as one accountThe caller, on every append
Event source type (EventSourceType)The kind of event source, such as AccountThe definition’s name
Event stream type (EventStreamType)A named process of the event source, such as TransactionsThe name of a stream the definition declares
Event stream id (EventStreamId)One instance of a stream, such as 2026-04The caller. Chronicle imposes no format

Each event also records the name of the definition it was appended through, in EventContext.EventSource. It is empty for events that were appended without a definition.

A definition is not an aggregate, and it is not a set of tags. The guard it derives does cover a whole event source or stream, whatever the event type, so it behaves like an aggregate boundary: choose it when the stream is the consistency boundary, and use a decision read when a rule needs a narrower condition. It does not list the event types that belong to it, and it does not own an event type: the same event type can be appended through any definition. Each event has exactly one event source and at most one stream.

Use one when:

  • Events of one kind of entity are written from several places and the routing strings must agree.
  • An entity has independent sub-histories, such as one stream per month, per author or per process step, and you want writers on different sub-histories not to conflict.
  • You want observers that handle only one stream.
  • You want to close one stream and keep the rest of the entity open. See Close a stream.

Skip it when every event of an entity can share one history and one guard. The plain append APIs are still valid and need no definition. See Questions and pitfalls.

Event source definitions are supported by the .NET, Kotlin, TypeScript and Elixir clients. The Python client has no definition API. The Kotlin, TypeScript, Elixir and Python packages under Source/Clients in this repository only hold generated kernel contracts. The examples in this section are .NET; the other clients express the same concepts in their own API, so see their repositories for the syntax. The kernel contract that registers definitions is language neutral, and the Workbench lists what is registered. Where the clients differ covers the append and concurrency differences that exist today.

You want toRead
Declare an event source and its streamsDefine an event source
Write events through a definitionAppend through a definition
Choose what conflicts with whatConcurrency
End a streamClose a stream
Let a reactor or reducer see one streamFilter observers
Move from [EventSourceType] and [EventStreamType] stringsMigrate from string routing
See complete examplesScenarios
Understand registration, metadata and scope productionHow it works
Use a definition with a decision model and an append conditionDynamic Consistency Boundary
Find a quick answer or a pitfallQuestions and pitfalls

For the concept behind these terms, read Event source. For how definitions relate to decision models and append conditions, and what Chronicle does and does not share with DCB, read Dynamic Consistency Boundary in Chronicle.