Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Eventuous/eventuous/llms.txt

Use this file to discover all available pages before exploring further.

Eventuous is built on a small set of well-defined concepts that map directly to Domain-Driven Design and Event Sourcing principles. Understanding these building blocks — and how they relate to each other — will help you build maintainable, testable, and evolvable systems. The sections below describe each concept, its role in the architecture, and the Eventuous type that implements it.

Events

Events are the source of truth in an event-sourced system. Every state change is represented as an immutable domain event appended to an event stream. Nothing is ever overwritten or deleted — the history of what happened is permanent. In Eventuous, events are plain C# record types decorated with [EventType] to give them a stable, version-independent name in the store:
[EventType("V1.RoomBooked")]
public record RoomBooked(
    string         GuestId,
    string         RoomId,
    LocalDate      CheckInDate,
    LocalDate      CheckOutDate,
    float          BookingPrice,
    float          PrepaidAmount,
    float          OutstandingAmount,
    string         Currency,
    DateTimeOffset BookingDate
);
Events are named in the past tense and carry all data needed to reconstruct state — they should never reference external resources or mutable objects.

Aggregate

The aggregate is the consistency boundary for a group of related entities. All business rules and invariants are enforced inside the aggregate. State changes produce events; events are the only way state ever changes. Eventuous provides the generic Aggregate<TState> base class:
public abstract class Aggregate<T> where T : State<T>, new() {
    public T State { get; private set; } = new();

    public IReadOnlyCollection<object> Changes => _changes.AsReadOnly();

    public long OriginalVersion { get; private set; } = -1;
    public long CurrentVersion  => OriginalVersion + Changes.Count;

    protected (T PreviousState, T CurrentState) Apply<TEvent>(TEvent evt)
        where TEvent : class;

    protected void EnsureDoesntExist(Func<Exception>? getException = null);
    protected void EnsureExists(Func<Exception>? getException = null);
}
When you call Apply(evt), Eventuous:
  1. Appends the event to the internal Changes list.
  2. Folds the event into State by calling State.When(evt).
When the aggregate is loaded from the store, all persisted events are replayed through State.When() to rebuild the current state deterministically. A real aggregate method looks like this:
public class Booking : Aggregate<BookingState> {
    public async Task BookRoom(
        string guestId, RoomId roomId, StayPeriod period,
        Money price, Money prepaid, DateTimeOffset bookedAt,
        IsRoomAvailable isRoomAvailable
    ) {
        EnsureDoesntExist(); // guard: stream must not already exist
        var roomAvailable = await isRoomAvailable(roomId, period);
        if (!roomAvailable) throw new DomainException("Room not available");

        Apply(new BookingEvents.V1.RoomBooked(
            guestId, roomId, period.CheckIn, period.CheckOut,
            price.Amount, prepaid.Amount, (price - prepaid).Amount,
            price.Currency, bookedAt
        ));
    }
}

State

State is a pure, immutable record that knows how to fold a sequence of domain events into the current representation of an aggregate. It has no behaviour — that lives in the aggregate. State is rebuilt from scratch every time an aggregate is loaded. Derive from State<T> and register event handlers with On<TEvent>() in the constructor:
public abstract record State<T> where T : State<T> {
    public virtual T When(object @event) { ... }

    protected void On<TEvent>(Func<T, TEvent, T> handle);
}
A concrete state type uses C# with expressions to produce new instances — records are never mutated:
public record BookingState : State<BookingState> {
    public Money Outstanding { get; init; } = null!;
    public bool  Paid        { get; init; }

    public BookingState() {
        On<BookingEvents.V1.RoomBooked>((state, e) => state with {
            Outstanding = new() { Amount = e.OutstandingAmount, Currency = e.Currency }
        });
        On<BookingEvents.V1.BookingFullyPaid>((state, _) => state with { Paid = true });
    }
}
There is also State<T, TId> for states that carry an identity property populated automatically by the infrastructure when loading from a stream.

Identity

Every aggregate instance is identified by a typed identity — a strongly-typed wrapper around a string. This prevents accidentally mixing up IDs from different aggregate types at compile time. Derive from the Id abstract record:
public abstract record Id {
    protected Id(string value) {
        if (string.IsNullOrWhiteSpace(value))
            throw new Exceptions.InvalidIdException(this);
        Value = value;
    }

    public string Value { get; }

    public sealed override string ToString() => Value;
    public static implicit operator string(Id? id) => id?.ToString() ?? ...;
}
Declare your own identity with a single-line record:
public record BookingId(string Value) : Id(Value);
The identity determines the stream name used in the event store (e.g., Booking-abc123).

Command Service

The command service is the application-layer entry point. It receives a command, loads the appropriate aggregate from the event store, executes business logic on it, and persists any new events — all in a single unit of work. The strongly-typed base class is CommandService<TAggregate, TState, TId>:
public class BookingsCommandService
    : CommandService<Booking, BookingState, BookingId> {

    public BookingsCommandService(
        IEventStore store,
        Services.IsRoomAvailable isRoomAvailable
    ) : base(store) {
        On<BookRoom>()
            .InState(ExpectedState.New)          // stream must not exist
            .GetId(cmd => new BookingId(cmd.BookingId))
            .ActAsync((booking, cmd, _) => booking.BookRoom(...));

        On<RecordPayment>()
            .InState(ExpectedState.Existing)     // stream must exist
            .GetId(cmd => new BookingId(cmd.BookingId))
            .Act((booking, cmd) => booking.RecordPayment(...));
    }
}
Each On<TCommand>() registration defines:
  • InState — whether to create a new aggregate (ExpectedState.New) or load an existing one (ExpectedState.Existing).
  • GetId — a function to extract the aggregate identity from the command.
  • Act / ActAsync — the delegate that calls the aggregate’s domain method.
A stateless variant CommandService<TState> is available when you do not need a full aggregate class.

Event Store

The event store is the persistence abstraction for event streams. Eventuous defines three complementary interfaces:
InterfaceResponsibility
IEventStoreCombined reader and writer
IEventReaderLoad events for a stream by name
IEventWriterAppend new events to a stream with optimistic concurrency
Swap implementations without touching your domain or application code:
// KurrentDB
services.AddEventStore<KurrentDBEventStore>();

// PostgreSQL
services.AddEventStore<PostgresEventStore>();

Subscriptions

Subscriptions are durable, checkpointed event consumers. They read from an event stream (or all streams) and deliver events to one or more event handlers. Checkpoints are persisted so that processing resumes from where it left off after a restart, providing at-least-once delivery guarantees.
services.AddSubscription<AllStreamSubscription, AllStreamSubscriptionOptions>(
    "BookingsProjections",
    builder => builder
        .UseCheckpointStore<MongoCheckpointStore>()
        .AddEventHandler<BookingStateProjection>()
        .AddEventHandler<MyBookingsProjection>()
        .WithPartitioningByStream(2)
);
Eventuous ships subscription implementations for KurrentDB (all-stream and persistent), Kafka, RabbitMQ, Google Cloud Pub/Sub, and Azure Service Bus.

Projections

Projections are event handlers that build read models. They consume events from a subscription and write denormalised views to a query-optimised store (e.g., MongoDB, Postgres). Each projection implements IEventHandler and focuses on a single concern.
public class BookingStateProjection : MongoProjection<BookingDocument> {
    public BookingStateProjection(IMongoDatabase database) : base(database) {
        On<BookingEvents.V1.RoomBooked>(
            evt => evt.Data.BookingId,
            (doc, evt) => doc with { RoomId = evt.Data.RoomId }
        );
    }
}

Producers

Producers publish events or messages to external message brokers. They implement the IProducer interface and are used either directly or via the Gateway — an event routing component that subscribes to an event store stream and forwards selected events to a broker.

Explore each concept in depth

Aggregates

The Aggregate<TState> base class, Apply, versioning, and guard methods.

State

Immutable state records, On<TEvent>() handlers, and event folding.

Command Services

Command registration, expected state, and the full request lifecycle.

Event Store

IEventStore abstraction and available backend implementations.

Subscriptions

Durable subscriptions, checkpoint stores, and partitioning.

Projections

Building and updating read models with MongoDB and other stores.

Build docs developers (and LLMs) love