Ruby Reactor 0.4.0 introduces interrupts — the first native saga pause/resume mechanism in the Ruby ecosystem.
You've built a checkout flow. The user submits their order, you reserve inventory, charge their card, and generate a shipping label. Everything's a Sidekiq job. It works.
Then you get the requirement: "After payment, wait for the fraud detection webhook before shipping."
Suddenly your clean Sidekiq pipeline needs to stop, hold state for an indeterminate amount of time, and resume when Stripe calls back. Your options are:
- Polling loop (wasteful, fragile)
- Split into two jobs (now you have to manage state yourself)
- Temporal / AWS Step Functions (massive infrastructure lift)
None of these feel right. This is where Ruby Reactor interrupts come in.
What is an Interrupt?
An interrupt is a special step that pauses reactor execution mid-flight, persists the entire execution state to Redis, and waits for an external signal to resume. While paused, no Sidekiq jobs are running. No polling. No wasted resources.
class FraudCheckReactor < 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 until Stripe calls back
interrupt :wait_for_fraud_check do
wait_for :charge_card
correlation_id { |ctx| "order-#{ctx.input(:order_id)}" }
timeout 3600, strategy: :active
validate do
required(:status).filled(:string, included_in?: %w[passed failed])
end
max_attempts 3
end
step :ship_order do
argument :status, result(:wait_for_fraud_check, :status)
run do |args|
args[:status] == "passed" ? Shipping.create_label : Failure("Fraud check failed")
end
undo { |_err, args| Shipping.cancel(args[:order_id]) }
end
returns :ship_order
end
How It Works
1. Start the reactor:
execution = FraudCheckReactor.run(order_id: 42)
execution.status # => :paused
The reactor runs reserve_inventory → charge_card → then pauses at wait_for_fraud_check. Everything before the interrupt is committed. The execution state is serialized to Redis.
2. Resume when the webhook arrives:
# In your Stripe webhook controller
FraudCheckReactor.continue_by_correlation_id(
correlation_id: "order-42",
payload: { status: "passed" },
step_name: :wait_for_fraud_check
)
The reactor wakes up, validates the payload (using the validate schema), feeds the payload as the interrupt's result, and continues to ship_order.
Why This Matters
The interrupt pattern solves a class of problems that previously forced Ruby developers into bad choices:
| Without interrupts | With interrupts |
|---|---|
| Polling loops that burn DB/Redis | Zero resource usage while waiting |
| Manual state management across jobs | Reactor handles all state persistence |
| Hard-to-debug split job chains | Single reactor definition = single mental model |
| No timeout handling (orphaned states) |
timeout with :active or :lazy strategies |
| No validation on resume payloads |
validate block validates incoming data |
Real-World Use Cases
Webhook-driven workflows:
- Stripe payment confirmation → interrupt → continue on
payment_intent.succeeded - External KYC provider → interrupt → continue on verification complete
- Async report generation → interrupt → continue when report URL is ready
Human-in-the-loop:
- Manager approval for large transactions
- Customer confirmation for subscription changes
- Support agent review for flagged accounts
Long-running external jobs:
- Video transcoding (submit → interrupt → continue on complete)
- ML model training (kick off → interrupt → continue on model ready)
Comparison: How Others Handle This
| Tool | Pause/Resume | Mechanism |
|---|---|---|
| Ruby Reactor 0.4.0 | ✅ Built-in interrupt
|
Correlation ID, Redis state, timeout |
| dry-transaction | ❌ No support | — |
| Trailblazer | ❌ Manual only | Custom state machine required |
| Temporal (Go/Node) | ✅ Native | Signal-based, built into engine |
| AWS Step Functions | ✅ Native | Task token, built into service |
Ruby Reactor is the only Ruby-native library that offers this pattern without pulling in external workflow engines.
Getting Started
gem install ruby_reactor
gem 'ruby_reactor', '~> 0.4'
Full interrupt docs: github.com/arturictus/ruby_reactor
If you're building workflows that need to pause and wait for external events, give Ruby Reactor a ⭐ and try it in your next Rails app.
Discussion questions: What's your current approach to webhook-driven workflows in Ruby? Polling? Split jobs? Let me know in the comments. 👇
Top comments (0)