Key Concepts
Overview
This page defines every concept used in the Spring sample. Each concept maps to a specific base class or interface in the Mississippi framework. Read this page first if you are new to event sourcing or to Mississippi.
Domain Terms → Framework Types
Use this table as a translation guide from plain-language architecture terms to concrete Mississippi types and attributes.
| Plain-Language Term | Spring Domain Artifact | Mississippi Type / Attribute | Meaning in the Framework |
|---|---|---|---|
| Command | DepositFunds, WithdrawFunds, OpenAccount | [GenerateCommand] | A request DTO that expresses intent and is used to generate API/client dispatch surfaces. |
| Command handler | DepositFundsHandler | CommandHandlerBase<TCommand, TSnapshot> | Validates business rules against current aggregate state and returns events via OperationResult. |
| Event | FundsDeposited, FundsWithdrawn | [EventStorageName] | Immutable fact persisted to the brook stream and replayed to rebuild state. |
| Event reducer | FundsDepositedReducer | EventReducerBase<TEvent, TProjection> / IEventReducer<...> | Pure state transition function that applies an event to current state. |
| Aggregate | BankAccountAggregate | [GenerateAggregateEndpoints], [BrookName], [SnapshotStorageName] | Domain state root that command handlers validate against and event reducers update. |
| Effect (synchronous) | HighValueTransactionEffect | SimpleEventEffectBase<TEvent, TAggregate> | Side-effect that runs after event persistence and can block command completion. |
| Effect (fire-and-forget) | WithdrawalNotificationEffect | FireAndForgetEventEffectBase<TEvent, TAggregate> | Side-effect executed asynchronously in worker infrastructure after event persistence. |
| Saga | MoneyTransferSagaState | ISagaState, [GenerateSagaEndpoints] | Long-running orchestration state for multi-step workflows across aggregates. |
| Saga step | WithdrawFromSourceStep | ISagaStep<TSaga>, [SagaStep<TSaga>(index)] | One executable workflow step with ordered execution. |
| Saga compensation | WithdrawFromSourceStep.CompensateAsync | ICompensatable<TSaga> | Optional rollback behavior invoked when a later step fails. |
| Projection (domain model) | BankAccountBalanceProjection | [GenerateProjectionEndpoints], [ProjectionPath], [BrookName] | Read-optimized domain projection record built from events. |
| UX projection runtime | Generated projection query/subscription runtime | IUxProjectionGrain<TProjection> / UxProjectionGrain<TProjection> | Orleans runtime abstraction that serves projection queries and versioned projection reads. |
| Brook | SPRING/BANKING/ACCOUNT | [BrookName], BrookKey | Canonical event stream identity (name + entity key). |
Notes:
- There is no single type literally named
UXProjection; the runtime projection abstraction is theIUxProjectionGrain<TProjection>contract. - For Spring domain-first learning, focus first on the
Spring.Domainrows (Command,CommandHandler, event,EventReducer, aggregate, effect, saga, projection). Runtime and host wiring details come after.
Key framework references:
CommandHandlerBase<TCommand, TSnapshot>EventReducerBase<TEvent, TProjection>IEventReducerISagaStateISagaStep<TSaga>andICompensatable<TSaga>SimpleEventEffectBase<TEvent, TAggregate>FireAndForgetEventEffectBase<TEvent, TAggregate>GenerateCommandAttributeGenerateAggregateEndpointsAttributeGenerateProjectionEndpointsAttributeGenerateSagaEndpointsAttributeProjectionPathAttributeIUxProjectionGrain<TProjection>UxProjectionGrain<TProjection>
Command
A command is an instruction to do something. It is a simple data record that carries the intent and parameters of an action. Commands do not contain logic - they describe what the caller wants to happen.
In Spring, commands are sealed record types decorated with [GenerateCommand] to auto-generate API endpoints and client-side dispatchers.
[GenerateCommand(Route = "deposit")]
[GenerateSerializer]
public sealed record DepositFunds
{
[Id(0)]
public decimal Amount { get; init; }
}
A command says "deposit £500." It does not decide whether the deposit is allowed.
Command Handler
A command handler receives a command and the current aggregate state, validates business rules, and returns either a list of events (success) or an error (failure). Command handlers extend CommandHandlerBase<TCommand, TSnapshot>.
CommandHandlers are pure decision-makers. They inspect the command and state, apply business rules, and produce events. They do not mutate state, call databases, or perform side effects.
internal sealed class DepositFundsHandler : CommandHandlerBase<DepositFunds, BankAccountAggregate>
{
protected override OperationResult<IReadOnlyList<object>> HandleCore(
DepositFunds command,
BankAccountAggregate? state)
{
if (state?.IsOpen != true)
return OperationResult.Fail<IReadOnlyList<object>>(
AggregateErrorCodes.InvalidState,
"Account must be open before depositing funds.");
if (command.Amount <= 0)
return OperationResult.Fail<IReadOnlyList<object>>(
AggregateErrorCodes.InvalidCommand,
"Deposit amount must be positive.");
return OperationResult.Ok<IReadOnlyList<object>>(
new object[] { new FundsDeposited { Amount = command.Amount } });
}
}
The CommandHandler decides: "Is the account open? Is the amount positive?" If yes, it emits a FundsDeposited event. If no, it returns an error. It never changes state directly.
Event
An event is an immutable fact that something happened. Events are the source of truth in an event-sourced system. They are stored permanently and replayed to reconstruct state.
Events are internal sealed record types decorated with [EventStorageName] to define their storage identity. Events are internal because external consumers read projections, not raw events.
[EventStorageName("SPRING", "BANKING", "FUNDSDEPOSITED")]
[GenerateSerializer]
internal sealed record FundsDeposited
{
[Id(0)]
public decimal Amount { get; init; }
}
An event says "£500 was deposited." It is a statement of fact - past tense, immutable, permanent.
EventReducer
An EventReducer is a pure function that takes the current state and an event, and returns the new state. EventReducers extend EventReducerBase<TEvent, TProjection> and implement IEventReducer contracts.
EventReducers have no side effects, no dependencies, no I/O. They receive state + event and return new state. This makes them trivially testable and deterministic.
internal sealed class FundsDepositedReducer : EventReducerBase<FundsDeposited, BankAccountAggregate>
{
protected override BankAccountAggregate ReduceCore(
BankAccountAggregate state,
FundsDeposited @event)
{
return (state ?? new()) with
{
Balance = (state?.Balance ?? 0) + @event.Amount,
DepositCount = (state?.DepositCount ?? 0) + 1,
};
}
}
The event reducer applies the fact: "A deposit happened, so increase the balance and increment the counter."
Aggregate
An aggregate is the state record that EventReducers build by replaying events. It is the internal, authoritative representation of an entity. Aggregates are decorated with [BrookName] to define their event stream identity and [GenerateAggregateEndpoints] to auto-generate API and grain infrastructure.
[BrookName("SPRING", "BANKING", "ACCOUNT")]
[GenerateAggregateEndpoints]
[GenerateSerializer]
public sealed record BankAccountAggregate
{
[Id(0)] public decimal Balance { get; init; }
[Id(1)] public bool IsOpen { get; init; }
[Id(2)] public string HolderName { get; init; } = string.Empty;
[Id(3)] public int DepositCount { get; init; }
[Id(4)] public int WithdrawalCount { get; init; }
}
Aggregates are never exposed to external consumers. Read-optimized projections serve that purpose.
For this Spring domain-first walkthrough, focus on EventReducer types in Spring.Domain. Client-side ActionReducer types are part of UI state management in the client host, not domain business logic. See Host Applications for where client state wiring lives.
Effect
An effect is a side-effect that runs in reaction to an event after the event has been persisted. Effects handle things that are not part of the core state transition: sending notifications, dispatching commands to other aggregates, and calling external APIs. Effects can run in either blocking (SimpleEventEffectBase) or fire-and-forget (FireAndForgetEventEffectBase) mode.
Mississippi provides two effect types:
| Type | Base Class | Behavior |
|---|---|---|
| Simple effect | SimpleEventEffectBase<TEvent, TAggregate> | Runs on the aggregate event-effect pipeline after the event is persisted. Use it for side operations that stay coupled to the aggregate flow without yielding additional events. |
| Fire-and-forget effect | FireAndForgetEventEffectBase<TEvent, TAggregate> | Runs in a separate worker grain. Use it for side effects that should be decoupled from the aggregate pipeline. |
Spring uses both:
HighValueTransactionEffect(simple) - flags deposits over £10,000 by dispatching a command to theTransactionInvestigationQueueAggregate.WithdrawalNotificationEffect(fire-and-forget) - sends a notification after withdrawals without blocking the response.
(HighValueTransactionEffect.cs | WithdrawalNotificationEffect.cs)
Saga
A saga orchestrates a long-running workflow that spans multiple aggregates. Each saga has a state record that implements ISagaState and is decorated with [GenerateSagaEndpoints].
[BrookName("SPRING", "BANKING", "TRANSFER")]
[GenerateSagaEndpoints(
InputType = typeof(StartMoneyTransferCommand),
RoutePrefix = "money-transfer",
FeatureKey = "moneyTransfer")]
[GenerateSerializer]
public sealed record MoneyTransferSagaState : ISagaState
{
[Id(0)] public Guid SagaId { get; init; }
[Id(1)] public SagaPhase Phase { get; init; }
[Id(2)] public int LastCompletedStepIndex { get; init; } = -1;
// ... additional state fields
}
The saga itself does not contain step logic. Steps are separate classes.
Saga Step
A saga step implements ISagaStep<TSaga> and executes one unit of work in the saga. Steps are ordered by the [SagaStep<TSaga>(index)] attribute.
[SagaStep<MoneyTransferSagaState>(0)]
internal sealed class WithdrawFromSourceStep : ISagaStep<MoneyTransferSagaState>
{
public async Task<StepResult> ExecuteAsync(
MoneyTransferSagaState state,
CancellationToken cancellationToken)
{
// Withdraw from source account
}
}
Steps return StepResult.Succeeded() or StepResult.Failed(...). If a step fails, the saga triggers compensation on previously completed steps.
Saga Compensation
A saga step that implements ICompensatable<TSaga> provides a CompensateAsync method. If a later step fails, the framework calls CompensateAsync on previously completed compensatable steps in reverse order to undo their work.
[SagaStep<MoneyTransferSagaState>(0)]
internal sealed class WithdrawFromSourceStep
: ISagaStep<MoneyTransferSagaState>,
ICompensatable<MoneyTransferSagaState>
{
public async Task<CompensationResult> CompensateAsync(
MoneyTransferSagaState state,
CancellationToken cancellationToken)
{
// Deposit the amount back to source account to undo the withdrawal
}
}
Not every step needs compensation. The DepositToDestinationStep in Spring does not implement ICompensatable because it is the final step - there is nothing after it that could fail.
(WithdrawFromSourceStep.cs | DepositToDestinationStep.cs)
Saga Command
A saga command is the input that starts a saga. It is a regular command record that carries the data the saga needs. The saga state captures this input so that all steps can access it.
[GenerateCommand(Route = "transfer")]
[GenerateSerializer]
public sealed record StartMoneyTransferCommand
{
[Id(0)] public string SourceAccountId { get; init; } = string.Empty;
[Id(1)] public string DestinationAccountId { get; init; } = string.Empty;
[Id(2)] public decimal Amount { get; init; }
}
(StartMoneyTransferCommand.cs)
Projection
A projection is a read-optimized view built from an event stream. Projections subscribe to the same events as aggregates but maintain their own state records tailored for specific read patterns. They are decorated with [ProjectionPath], [BrookName], and [GenerateProjectionEndpoints].
[ProjectionPath("bank-account-balance")]
[BrookName("SPRING", "BANKING", "ACCOUNT")]
[GenerateProjectionEndpoints]
[GenerateSerializer]
public sealed record BankAccountBalanceProjection
{
[Id(0)] public decimal Balance { get; init; }
[Id(1)] public string HolderName { get; init; } = string.Empty;
[Id(2)] public bool IsOpen { get; init; }
}
Projections have their own EventReducers. A single event stream can feed multiple projections - the BankAccount event stream feeds both BankAccountBalanceProjection and BankAccountLedgerProjection.
(BankAccountBalanceProjection.cs)
Brook
A brook is a named event stream. The [BrookName("SPRING", "BANKING", "ACCOUNT")] attribute defines a three-part hierarchical name. Multiple aggregates and projections can share the same brook name when they consume the same event stream. The BrookKey type combines the brook name with a specific entity ID at runtime.
How Concepts Connect
- A command is sent to a command handler.
- The command handler validates rules against the current state and emits events on success.
- EventReducers apply events to the aggregate state.
- Effects react to events asynchronously (notifications, cross-aggregate commands).
- Projection EventReducers build read-optimized projection state from the same events.
Summary
Every concept in Mississippi has a single, well-defined responsibility. Commands carry intent. Handlers validate rules. Events record facts. EventReducers compute event-sourced state. Effects trigger side actions. Sagas coordinate multi-step workflows. Projections build read views. This separation keeps each piece small, testable, and replaceable.
Next Steps
- Building an Aggregate - See these concepts in action with the BankAccount example
- Building a Saga - Orchestrate a multi-aggregate workflow
- Building Projections - Create read-optimized views