Hybrid EventFlow Adoption: CQRS-Only and Event Sourcing Strategy"
Hello, how are you?
I'm currently using EventFlow in one of our projects and am now looking to expand its adoption across additional applications. However, I'd like to implement a granular event sourcing strategy — meaning that different parts of the application would adopt different levels of event sourcing.
For example, one part of the system might only use CQRS for now, potentially adopting full Event Sourcing (ES) later, while another part might fully adopt ES from the beginning. This behavior would be controlled at the aggregate root level. Here's what I mean:
We have two aggregate roots:
Account
Payments
For the Account aggregate, I plan to use full Event Sourcing. It's a small aggregate where I need a clear, auditable history of all changes.
For the Payments aggregate, the scenario is different. It’s also a simple aggregate, but the only operation is to record a payment. In this case, using full event sourcing would just result in a single event for creation. So, it makes more sense to go with a state-based model, leveraging CQRS without full ES. Still, I want to keep all the benefits of EventFlow — like command idempotency, domain event publishing, and subscribers.
My question is: Is there an established or recommended way to implement this kind of hybrid model in EventFlow?
From what I’ve explored so far, it seems I might need to implement custom versions of IAggregateRoot and IAggregateStore to support both flows.
up @rasmus
Hello there!
We hope you are doing well. We noticed that this issue has not seen any activity in the past 90 days. We consider this issue to be stale and will be closing it within the next seven days.
If you still require assistance with this issue, please feel free to reopen it or create a new issue.
Thank you for your understanding and cooperation.
Best regards, EventFlow
Thanks for raising this! EventFlow always persists aggregates by emitting domain events, AggregateRoot stores them through the IEventStore, and AggregateStore rehydrates and commits that stream. There isn’t a built-in switch to make a specific aggregate “CQRS-only” without events. You’d need custom aggregate/aggregate-store plumbing to bypass the event stream while still handling concurrency, history, and publishing.
If you only need a simple view for something like Payments, the usual options are:
- Keep emitting the (minimal) events so you retain snapshots, retries, subscribers, etc. The overhead stays small even if the aggregate is simple.
- Skip
AggregateRootaltogether for those pieces, handle the commands directly, persist state however you want, and publish whatever integration events you need yourself.
Trying to mix both models inside the same aggregate layer tends to get messy, so most stick to one of the two approaches depending on how much history they need.
Thanks for the detailed explanation, @rasmus — it makes things much clearer.
Given that mixing both models in the same aggregate layer tends to introduce complexity, I see the value in sticking to one of the two patterns you suggested. However, would it make sense to explore a lightweight AggregateRoot variant that doesn’t rely on event sourcing, but still fits into the same structural model?
In this approach, instead of calling Emit, certain aggregates could interact directly with a repository to persist state — skipping rehydration and snapshots entirely, since those wouldn’t be necessary. Emitting domain events for subscribers and sagas could be made optional, depending on the needs of each aggregate.
Do you think it's worth opening a PR to explore this direction, or would that go against the design principles of EventFlow?
.analyze
EventFlow’s aggregate pipeline is hard-wired around event streams. Any AggregateRoot<TAggregate,TIdentity> instance is created and later rehydrated exclusively through the event store (Source/EventFlow/Aggregates/AggregateStore.cs:84-86). The default base class then expects to (1) call LoadEventsAsync on the event store when loading and (2) push _uncommittedEvents to the event store in CommitAsync (Source/EventFlow/Aggregates/AggregateRoot.cs:122-150). Domain-event dispatching, snapshots, and duplicate-command detection all hang off the collection of events the aggregate committed.
What that means for a “CQRS-only” aggregate:
HasSourceId/PreviousSourceIdsrely on metadata coming back from stored events. If you skip committing events, the buffer never gets new source IDs, so idempotency fails on the next process restart (Source/EventFlow/Aggregates/AggregateRoot.cs:84-179).- Domain-event publishing is driven entirely by the
IDomainEventcollection returned fromCommitAsync. The publisher dispatches those events into read stores, subscribers, and sagas (Source/EventFlow/Subscribers/DomainEventPublisher.cs:33-102). If no events are persisted, nothing flows through that pipeline. - Current event-store implementations simply no-op if asked to commit an empty batch (
Source/EventFlow/EventStores/InMemory/InMemoryEventPersistence.cs:143-188), so you get no optimistic-concurrency protection, no source-id tracking, and no history to rebuild from.
Because of those couplings there isn’t a supported “state-only aggregate” alongside event-sourced ones out of the box. You technically could subclass AggregateRoot and override LoadAsync/CommitAsync, but you then have to re-implement all of the above concerns yourself (sequence numbers, metadata, idempotency cache, rebuilding state, domain-event creation, etc.). That ends up effectively rewriting a parallel persistence stack, along with custom IAggregateStore wiring to decide which persistence path to take per aggregate type—the comment in the issue is spot on.
Practical options:
- Keep every aggregate event-sourced, even trivial ones. A simple
PaymentRecordedevent still buys you history, replayability, and automatic read-model updates while staying within the supported pipeline. - If you truly want a state-based write model, handle that bounded context outside of
AggregateRoot/IAggregateStore—e.g., register a plainICommandHandlerthat talks to your persistence directly. You lose EventFlow’s aggregate conveniences (source-id dedupe, event replay, saga integration), but you avoid fighting the internals. - A genuine hybrid would require a substantial extension: custom aggregate base class + store that persists a state snapshot plus synthetic domain events (with proper metadata) so the rest of the pipeline keeps working. That is currently non-trivial and would need careful design to avoid regressions.
So, there’s no established recipe today; the recommended path is still “event-source the areas where you want EventFlow’s guarantees,” and leave purely state-based pieces outside the aggregate pipeline if you can’t accept the extra event. Your friendly neighborhood AI