Skip to content

Feature Proposal: Declarative Saga Builder API #1135

@gnios

Description

@gnios

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 AggregateSaga with a new DeclarativeSaga base class
  • Still requires ISagaIsStartedBy and ISagaHandles interfaces (EventFlow's discovery mechanism)
  • Uses EventFlow's existing job scheduling for timeouts
  • State management uses EventFlow's event sourcing pattern

Questions

  1. Does this align with EventFlow's philosophy?
  2. Should this be a separate package or part of core?
  3. 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? 🙏

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions