An aggregate is the consistency boundary in your domain model. In Eventuous, every aggregate inherits fromDocumentation 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.
Aggregate<T> where T is a State<T> record that carries all mutable data. The aggregate class itself holds only the event-application logic and business invariants — it never stores raw field values. When a domain method runs, it records what happened by emitting an event; the event is both appended to the list of pending changes and immediately applied to produce a new State snapshot.
Class signature
T must be a concrete record that derives from State<T>. Because T has a new() constraint, Eventuous can construct the initial empty state without any factory or DI involvement.
Key properties
| Property | Type | Description |
|---|---|---|
State | T | The current aggregate state (read-only from outside) |
Original | object[] | Events loaded from the store; empty for a brand-new aggregate |
Changes | IReadOnlyCollection<object> | Pending new events produced in the current operation |
Current | IEnumerable<object> | Original concatenated with Changes — the full event sequence |
OriginalVersion | long | Version at load time; -1 if the aggregate has never been persisted |
CurrentVersion | long | OriginalVersion + Changes.Count — increases with every Apply call |
OriginalVersion is what Eventuous passes to the event store for optimistic concurrency: if another writer has appended events since this aggregate was loaded, the store will reject the append with a concurrency conflict.
Protected methods
Apply<TEvent>(TEvent evt)
Apply inside every domain method to record that something happened. It does three things atomically:
- Calls
AddChange(evt)to queue the event inChanges. - Captures the current
StateasPreviousState. - Calls
State.When(evt)to produce the next immutable state snapshot.
AddChange(object evt)
Changes without transitioning state. Prefer Apply<TEvent> for domain methods; AddChange is a lower-level escape hatch for cases where you have already transitioned state yourself.
Load(long version, IEnumerable<object?> events)
- Filters out any
nullevents (unknown or undeserializable event types). - Sets
OriginalVersiontoversion. - Folds all events through
State.When()to build the current state. - Stores the raw events in
Original.
Load, Changes is empty and CurrentVersion == OriginalVersion.
EnsureDoesntExist() and EnsureExists()
EnsureDoesntExist() at the start of a creation method — it throws DomainException (or your custom exception) if CurrentVersion >= 0, which means the aggregate has already been persisted. Call EnsureExists() before any update method — it throws if CurrentVersion < 0.
Both accept an optional getException factory so you can supply a domain-specific exception instead of the generic DomainException:
ClearChanges()
Changes list without affecting State or Original. This is useful in unit tests where you want to inspect the initial post-BookRoom state, then clear pending changes before exercising a second command.
Full example — Booking aggregate
The following aggregate is taken directly from the Eventuous sample application. Every public method corresponds to a single business operation; each operation validates an invariant, then records the outcome as a domain event viaApply.
The Apply pattern
The design principle at work here is:- Every public method encodes a decision.
BookRoomchecks availability and computes the outstanding balance; it does not store those values in fields. Applyrecords the outcome. The event carries all the facts that resulted from the decision. The event is both the persistence artifact and the state-transition trigger.- State is read back through
State. AfterApply(new V1.RoomBooked(...))returns,State.Outstandingalready reflects the new value becauseBookingStatehas anOn<V1.RoomBooked>handler. The next lineMarkFullyPaidIfNecessarycan immediately interrogateState.Outstanding.Amountto decide whether to emit aBookingFullyPaidevent. - Aggregates are never mutated directly. There are no settable properties on
Bookingitself. All writable data lives in theBookingStaterecord, which is replaced (not mutated) on everyApply.
State<T> records handle data.
If you need to read the previous state alongside the new one — for example, to detect a transition from “not paid” to “fully paid” — destructure the tuple returned by
Apply: