-
-
Notifications
You must be signed in to change notification settings - Fork 462
Description
Hey @rasmus and EventFlow community! 👋
I've been using EventFlow for a while now, and I've found that building complex sagas with multiple paths, timeouts, and compensation logic can get a bit messy. The workflow logic often gets scattered across multiple methods, making it hard to see the big picture at a glance.
The Idea
I've been working on a Declarative Saga Builder inspired by MassTransit's saga orchestration patterns. The idea is to provide a fluent, type-safe API for defining saga workflows, where the builder uses Interface Segregation to guide developers through valid configurations at compile time.
I'm currently validating this approach in a project at work, and it's been working really well so far. The code is much more readable and maintainable.
Example
Instead of manually handling events and timeouts across multiple methods, you could define the entire workflow in one place:
public class OrderSaga : DeclarativeSaga<OrderSaga, OrderSagaId, OrderSagaLocator>,
ISagaIsStartedBy<OrderAggregate, OrderId, OrderCreatedEvent>,
ISagaHandles<OrderAggregate, OrderId, OrderStockReservedEvent>,
ISagaHandles<OrderAggregate, OrderId, OrderPaymentCompletedEvent>
{
public OrderSaga(OrderSagaId id, IServiceProvider serviceProvider) : base(id, serviceProvider)
{
var state = RegisterState(new OrderSagaState());
Define(builder =>
{
// Start saga when order is created
builder.Initially()
.When<OrderAggregate, OrderId, OrderCreatedEvent>()
.ThenEmitSagaEvent((evt, saga) => new OrderSagaStartedEvent(...))
.ThenPublish<OrderAggregate, OrderId>((evt, saga) =>
new ReserveStockCommand(evt.AggregateIdentity));
// Timeout with automatic compensation
builder.When<OrderAggregate, OrderId, OrderStockReservedEvent>(timeoutMinutes: 10)
.ThenPublish<OrderAggregate, OrderId>((evt, saga) =>
new CompletePaymentCommand(evt.AggregateIdentity))
.AndCompensateWith<OrderAggregate, OrderId>((evt, saga) =>
new StockRollbackCommand(evt.AggregateIdentity));
// Success path
builder.When<OrderAggregate, OrderId, OrderPaymentCompletedEvent>()
.ThenEmitSagaEvent((evt, saga) => new OrderSagaCompletedEvent(...))
.AndComplete();
});
}
// Still need to implement interfaces - delegate to HandleEventAsync
public Task HandleAsync(IDomainEvent<OrderAggregate, OrderId, OrderCreatedEvent> e,
ISagaContext ctx, CancellationToken ct) => HandleEventAsync(e, ctx, ct);
public Task HandleAsync(IDomainEvent<OrderAggregate, OrderId, OrderStockReservedEvent> e,
ISagaContext ctx, CancellationToken ct) => HandleEventAsync(e, ctx, ct);
public Task HandleAsync(IDomainEvent<OrderAggregate, OrderId, OrderPaymentCompletedEvent> e,
ISagaContext ctx, CancellationToken ct) => HandleEventAsync(e, ctx, ct);
}Key Benefits
- ✅ Readability: Entire workflow visible in one place
- ✅ Type Safety: Compiler enforces valid configurations
- ✅ Less Boilerplate: Automatic timeout and compensation handling
- ✅ Backward Compatible: Completely optional - existing sagas continue to work
Implementation Approach
- New optional package (name to be decided)
- Extends
AggregateSagawith a newDeclarativeSagabase class - Still requires
ISagaIsStartedByandISagaHandlesinterfaces (EventFlow's discovery mechanism) - Uses EventFlow's existing job scheduling for timeouts
- State management uses EventFlow's event sourcing pattern
Questions
- Does this align with EventFlow's philosophy?
- Should this be a separate package or part of core?
- What would be a good name for the package/classes?
I'd love to get your thoughts on this idea! Since I'm already validating it in a real project, I have a good sense of what works and what doesn't. If this aligns with EventFlow's direction, I'd be happy to implement it properly and open a PR. What do you think? 🙏