Skip to main content

Your first aggregate

The Quickstart appended one event. This goes one step further into the parts that make Celeriant worth using: conditional writes and idempotency. The examples are .NET; the shape is the same in every client.

Create and append

An aggregate is addressed by org / type / id and comes into being on its first write:

var key = new AggregateKey(orgId, ordersType, orderId);

await pool.WriteAsync(key,
events: [new AggregateEvent
{
ClientSeq = 1,
EventTypeMajor = 1,
EventTimestamp = DateTimeOffset.UtcNow,
EventValue = Encoding.UTF8.GetBytes("""{ "sku": "A-1", "qty": 2 }"""),
}],
allowCreate: true);

Read it back, get the version

var details = await pool.AggregateDetailsAsync(new AggregateDetailsRequest { AggregateKey = key });
long version = details.MaxAggregateVersion; // where the aggregate is now

Write conditionally

The point of an event store: append only if nobody moved the aggregate since you read it. Pass the version you expect, and a stable clientId so a retry cannot double-write:

await pool.WriteAsync(key,
events: [new AggregateEvent
{
ClientSeq = 2,
EventTypeMajor = 2,
EventTimestamp = DateTimeOffset.UtcNow,
EventValue = Encoding.UTF8.GetBytes("""{ "event": "shipped" }"""),
}],
clientId: writerId,
expectedVersion: version,
enforceClientIdempotency: true);

If another writer got there first, this throws WriteOccException and nothing is appended. That is the guarantee you came for.

Next