u/arturictus

I built a saga orchestrator for Ruby — DAG execution, async Sidekiq, automatic rollback
▲ 14 r/rails+1 crossposts

I built a saga orchestrator for Ruby — DAG execution, async Sidekiq, automatic rollback

After wrestling with distributed transactions across microservices and watching Sidekiq job chains grow into unmaintainable spaghetti, I built Ruby Reactor — a saga pattern implementation for Ruby that handles the hard parts of workflow orchestration.

What it does:

  • Builds a DAG (directed acyclic graph) from your step definitions, so independent steps run in parallel
  • Runs async via Sidekiq with back-pressure and batching
  • Automatically rolls back completed steps when something fails (compensation)
  • Supports interrupts — pause a workflow mid-flight and resume it later via webhook or manual trigger
  • Includes a built-in web dashboard to inspect every execution
  • Has locks, semaphores, and rate limits built in (Redis-backed)
  • Ships with RSpec test helperstest_reactor, mock_step, chainable matchers

Why I built it:

I kept hitting the same wall: complex business transactions that span multiple services need coordination. dry-transaction handles linear pipelines well, but when you need parallel execution, async processing, automatic rollback on failure, or the ability to pause and wait for external events, you're on your own. Trailblazer operations can do some of this, but the undo logic and parallelism are manual.

Ruby Reactor fills the gap — it's the only Ruby library that combines DAG planning + async execution + compensation + interrupts + a dashboard in one package.

Quick example — an e-commerce checkout with fraud detection:

class CheckoutReactor < RubyReactor::Reactor
  input :order_id

  step :reserve_inventory do
    argument :order_id, input(:order_id)
    run { |args| Inventory.reserve(args[:order_id]) }
    undo { |_err, args| Inventory.release(args[:order_id]) }
  end

  step :charge_card do
    argument :order_id, input(:order_id)
    run { |args| Payment.charge(args[:order_id]) }
    undo { |_err, args| Payment.refund(args[:order_id]) }
  end

  # Pause here — wait for Stripe webhook
  interrupt :wait_for_fraud_check do
    wait_for :charge_card
    correlation_id { |ctx| "order-#{ctx.input(:order_id)}" }
    timeout 3600, strategy: :active
  end

  step :ship_order do
    argument :status, result(:wait_for_fraud_check, :status)
    run { |args| Shipping.create_label(args[:order_id]) }
    undo { |_err, args| Shipping.cancel(args[:order_id]) }
  end

  returns :ship_order
end

It's at v0.4.1 now with ~3,300 downloads on Rubygems. The v0.4.0 release just added interrupts, the web dashboard, RSpec helpers, and Redis-backed coordination primitives (locks, semaphores, rate limits, periods).

How it compares:

Feature Ruby Reactor dry-transaction Trailblazer Raw Sidekiq
DAG/Parallel execution Limited Manual
Auto compensation/undo Manual Manual
Interrupts (pause/resume) Manual
Built-in web dashboard
Locks / sem / rate limits Manual
Async with Sidekiq Limited

I'd love honest feedback — especially from people who've built complex workflows in production. What did I miss? What's over-engineered? What would make you actually use this instead of raw Sidekiq jobs?

Repo: https://github.com/arturictus/ruby_reactor Rubygems: gem 'ruby_reactor', '~> 0.5' Docs: Full guides for every feature in the repo's documentation/ directory

Thanks for reading! I'll be in the comments.

u/arturictus — 7 days ago