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
- Handling concurrency conflicts: the read-decide-write loop and the four ways a retry resolves.
- Implementing idempotent writes: why
clientIdmust be stable. - Building a read model: how you query, since Celeriant is the write side.