Skip to content

Event store

In order to isolate the core library from a particular way of storing events, Eventuous uses the IEventStore abstraction. Whilst it’s used by AggregateStore, you can also use it in a more generic way, when you need to persist or read events without having an aggregate.

The IEventStore interface inherits from IEventReader and IEventWriter interfaces. Each of those interfaces is focused on one specific task - reading events from streams, and appending events to streams. This separation is necessary for scenarios when you only need, for example, to read events from a specific store, but not to append them. In such case, you’d want to use the IEventReader interface only.

Eventuous has several implementations of event store abstraction, you can find them in the infrastructure section. The default implementation is KurrentDBEventStore, which uses KurrentDB as the event store. It’s a great product, and we’re happy to provide first-class support for it. It’s also a great product for learning about Event Sourcing and CQRS.

In addition, Eventuous has an in-memory event store, which is mostly used for testing purposes. It’s not recommended to use it in production, as it doesn’t provide any persistence guarantees.

Event store works with a couple of primitives, which allow wrapping infrastructure-specific structures. Those primitives are:

Record typeWhat it’s for
StreamReadPositionRepresent the stream revision, from there the event store will read the stream forwards or backwards.
ExpectedStreamVersionThe stream version (revision), which we expect to have in the database, when event store tries to append new events. Used for optimistic concurrency.
StreamEventA structure, which holds the event type as a string as well as serialised event payload and metadata.

All of those are immutable records.

Right now, we only have four operations for an event store:

FunctionWhat’s it for
AppendEventsAppend one or more events to a given stream.
AppendEvents (multi-stream)Append events to multiple streams in a single operation.
ReadEventsRead events from a stream forwards, from a given start position.

You can append events to multiple streams in a single operation using the multi-stream overload of AppendEvents:

NewStreamAppend[] appends = [
new(orderStream, ExpectedStreamVersion.NoStream, orderEvents),
new(inventoryStream, new ExpectedStreamVersion(currentVersion), inventoryEvents)
];
AppendEventsResult[] results = await eventStore.AppendEvents(appends, cancellationToken);

Each element specifies a target stream, its expected version, and the events to append. The return array contains one AppendEventsResult per stream in the same order as the input.

Atomicity guarantees vary by store:

StoreAtomicity
KurrentDB (25.1+)Atomic — all streams updated or entire operation fails
PostgreSQLAtomic — uses a single database transaction
SQL ServerAtomic — uses a single database transaction
SQLiteAtomic — uses a single database transaction
Default (other stores)Not atomic — streams are written sequentially, fails on first error

Eventuous has several implementations of the event store:

If you use one of the implementations provided, you won’t need to know about the event store abstraction. It is required though if you want to implement it for your preferred database.