Appending
Appending adds a single event to an event sequence. The event log is the default sequence, but you can also append to any custom sequence your system defines.
How it works
Section titled “How it works”When you append an event, Chronicle:
- Validates the event content against its registered JSON schema
- Applies any cross-cutting properties
- Associates the event with its event source
- Assigns the next sequence number
- Persists the event and metadata atomically
- Updates the sequence state
The append returns an operation result you can use to detect schema errors, concurrency violations, constraint violations, and other append-time problems. This makes it safe to retry or revise without duplicating events.
AppendResult
Section titled “AppendResult”Appending returns an AppendResult that describes whether the operation succeeded and includes details about what went wrong when it fails. It can surface schema validation errors, concurrency violations, constraint violations, and other append-time problems. See Concurrency and Constraints for more on these cases.
Schema validation
Section titled “Schema validation”Chronicle validates every event’s content against its registered JSON schema before persisting it. If the content does not conform to the schema — for example, a required property is missing or has the wrong type — the append fails and the errors appear in AppendResult.Errors.
var result = await eventLog.Append(eventSourceId, new OrderPlaced(customerId, total));
if (result.HasErrors){ foreach (var error in result.Errors) { Console.WriteLine($"Schema error: {error}"); }}val result = store.eventLog.append(eventSourceId, OrderPlaced(customerId, total))
if (!result.isSuccess) { result.errors.forEach { error -> println("Schema error: $error") }}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.eventSequences.AppendResult;import io.cratis.chronicle.events.EventType;import kotlinx.coroutines.BuildersKt;import kotlin.coroutines.EmptyCoroutineContext;import kotlin.coroutines.Continuation;
@EventType(id = "SchemaValidatedOrderPlaced")record SchemaValidatedOrderPlaced(String customerId, double total) {}
class SchemaValidationExample { void append(IEventStore store, String eventSourceId, String customerId, double total) throws InterruptedException { var result = (AppendResult) BuildersKt.runBlocking( EmptyCoroutineContext.INSTANCE, (scope, continuation) -> { @SuppressWarnings("unchecked") var appendContinuation = (Continuation<? super AppendResult>) continuation; return store.getEventLog().append( eventSourceId, new SchemaValidatedOrderPlaced(customerId, total), null, appendContinuation); });
if (!result.isSuccess()) { result.getErrors().forEach(error -> System.out.println("Schema error: " + error)); } }}case Chronicle.append(event_source_id, %MyApp.Events.OrderPlaced{ customer_id: customer_id, total: total }) do :ok -> :ok
{:error, {:append_errors, errors}} -> Enum.each(errors, &IO.puts("Schema error: #{inspect(&1)}"))endconst result = await store.eventLog.append( eventSourceId, new OrderPlaced(customerId, total));
if (!result.isSuccess) { for (const error of result.errors) { console.log(`Schema error: ${error.message}`); }}Schema validation runs before any other checks, so a schema error is returned immediately without touching the event sequence.
Specifying when an event occurred
Section titled “Specifying when an event occurred”By default, Chronicle assigns the current server time as the event’s Occurred timestamp when it persists the event. Some clients expose an explicit timestamp override for import and replay scenarios.
var result = await eventLog.Append( eventSourceId, new OrderPlaced(customerId, total), occurred: DateTimeOffset.Parse("2024-01-15T10:30:00Z"));{:ok, occurred, 0} = DateTime.from_iso8601("2024-01-15T10:30:00Z")
:ok = Chronicle.append( event_source_id, %MyApp.Events.OrderPlaced{customer_id: customer_id, total: total}, occurred: occurred )When to use
Section titled “When to use”Appending is lightweight and intended for single domain decisions that produce one event. When your decision spans multiple event sources, use concurrency scopes to enforce the boundary while keeping sequences independent. See Concurrency for details.
Example
Section titled “Example”using Cratis.Chronicle.Events;
[EventType]public record OrderPlaced(string CustomerId, decimal Total);
public class CheckoutService(IEventLog eventLog){ public async Task PlaceOrder(OrderId orderId, string customerId, decimal total) { var result = await eventLog.Append( orderId, new OrderPlaced(customerId, total) );
if (!result.IsSuccess) { // Decide whether to retry or surface a conflict to the caller. } }}import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.events.EventType
@EventType(id = "OrderPlaced")data class OrderPlaced( val customerId: String, val total: Double)
class CheckoutService(private val store: IEventStore) { suspend fun placeOrder(orderId: String, customerId: String, total: Double) { val result = store.eventLog.append( orderId, OrderPlaced(customerId, total) )
if (!result.isSuccess) { // Decide whether to retry or surface a conflict to the caller. } }}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.eventSequences.AppendResult;import io.cratis.chronicle.events.EventType;import kotlinx.coroutines.BuildersKt;import kotlin.coroutines.EmptyCoroutineContext;import kotlin.coroutines.Continuation;
@EventType(id = "OrderPlaced")record OrderPlaced(String customerId, double total) {}
class CheckoutService { private final IEventStore store;
CheckoutService(IEventStore store) { this.store = store; }
void placeOrder(String orderId, String customerId, double total) throws InterruptedException { var result = (AppendResult) BuildersKt.runBlocking( EmptyCoroutineContext.INSTANCE, (scope, continuation) -> { @SuppressWarnings("unchecked") var appendContinuation = (Continuation<? super AppendResult>) continuation; return store.getEventLog().append( orderId, new OrderPlaced(customerId, total), null, appendContinuation); });
if (!result.isSuccess()) { // Decide whether to retry or surface a conflict to the caller. } }}defmodule MyApp.Events.OrderPlaced do use Chronicle.Events.EventType, id: "order-placed"
defstruct [:customer_id, :total]end
defmodule MyApp.CheckoutService do def place_order(order_id, customer_id, total) do case Chronicle.append(order_id, %MyApp.Events.OrderPlaced{ customer_id: customer_id, total: total }) do :ok -> :ok
{:error, _reason} = error -> # Decide whether to retry or surface a conflict to the caller. error end endendimport { eventType, IEventStore } from '@cratis/chronicle';
@eventType()class OrderPlaced { constructor( readonly customerId: string, readonly total: number ) {}}
class CheckoutService { constructor(private readonly store: IEventStore) {}
async placeOrder(orderId: string, customerId: string, total: number): Promise<void> { const result = await this.store.eventLog.append( orderId, new OrderPlaced(customerId, total) );
if (!result.isSuccess) { // Decide whether to retry or surface a conflict to the caller. } }}