Write Model
Overview
Mississippi's write model is aggregate-centric.
One aggregate instance handles one command path for one entity ID at a time. The runtime loads the current aggregate state, asks command handlers to decide what should happen, persists the resulting events to the aggregate's brook, rebuilds state through reducers, and then runs any follow-on effects.
The Problem This Solves
The hard part of event-sourced writes is usually not emitting events. The hard part is keeping the command path, state reconstruction, side effects, and transport surfaces consistent.
Mississippi addresses that by making the write flow explicit and repeatable:
- state lives in an aggregate record
- command validation lives in
CommandHandlerBase<TCommand, TAggregate>implementations - event application lives in
IEventReducerandIRootReducerimplementations - generated gateway and client surfaces call the same underlying aggregate command path
Core Idea
Aggregate state is never mutated directly by commands.
Commands decide whether an aggregate may move from its current state to a new one. Events record that decision. Reducers then derive the new state from the persisted event stream.
How It Works
This diagram shows the aggregate write flow from command to persisted events and follow-on effects.
The concrete runtime sequence is:
- A generated endpoint, generated client action, or another runtime component routes the command to the aggregate instance for a specific entity ID.
IGenericAggregateGrain<TAggregate>loads the latest brook position and current aggregate state.IRootCommandHandler<TAggregate>dispatches the command to the matchingICommandHandlerimplementation.- The handler returns either an
OperationResultfailure or an ordered list of domain events. - The aggregate runtime appends those events to Brooks.
- Tributary reducers rebuild aggregate state from the event stream and snapshots.
- If a root event effect is registered, synchronous event effects run after persistence and may yield additional events.
- If fire-and-forget effect registrations exist, they are dispatched after persistence in separate worker grains and are not awaited by the command path.
Guarantees
- Commands do not write aggregate state directly. They succeed by returning events.
- Aggregate state is reconstructed through reducers, not by imperative mutation inside handlers.
GenericAggregateGrain<TAggregate>supports optimistic concurrency throughExecuteAsync(command, expectedVersion, ...).- Synchronous event effects run after the original events are persisted.
EventEffectBase<TEvent, TAggregate>effects may yield additional events, and the aggregate runtime persists those yielded events immediately.- Fire-and-forget effects use
FireAndForgetEventEffectBase<TEvent, TAggregate>and cannot yield additional events. - The aggregate runtime caps synchronous effect chaining through
AggregateEffectOptions.MaxEffectIterations, which defaults to10.
Non-Guarantees
- Mississippi does not provide a cross-aggregate transaction boundary. Each aggregate command runs against one aggregate instance at a time.
- Synchronous event effects do not make the original command atomic with downstream side effects. The original events are already persisted before those effects run.
- Fire-and-forget effects are intentionally not part of the request completion path. A successful command does not mean every background side effect has finished.
- The framework does not provide a global ordering guarantee across aggregates. Ordering is meaningful within a brook, not across the entire system.
Trade-Offs
- This model keeps domain decisions explicit, but it requires developers to think in events and reducers rather than direct state mutation.
- Synchronous effects are powerful because they can emit more events, but they also make it easier to create complex event chains. The iteration limit exists to stop accidental cycles.
- Fire-and-forget effects keep command latency low, but they shift some work into eventually observed background behavior.
Testability
This write model is designed to keep domain behavior testable without Orleans infrastructure.
Because command handlers return events instead of mutating state directly, the core business decision stays narrow and observable. Reducers rebuild state from those events, and effects run over explicit event inputs. That makes it practical to test aggregate flows with AggregateTestHarness<TAggregate> and AggregateScenario<TAggregate>, and to test synchronous or fire-and-forget effects with EffectTestHarness<...> and FireAndForgetEffectTestHarness<...>, using Given/When/Then style scenarios rather than TestCluster setup or grain mocking.
Related Tasks and Reference
- Use Read Models and Client Sync for the projection side of the same events.
- Use Sagas and Orchestration when one business operation spans several aggregates or steps.
- Use Domain Modeling for the package boundary around aggregates, effects, and test harnesses.
Summary
Mississippi's write model keeps domain decisions in handlers and state reconstruction in reducers, making the entire command path explicit, testable, and free of infrastructure coupling.