The core idea is three separate attribute layers: inputs (what comes in), internals (working state), and outputs (what goes out). Each is a distinct declaration with its own namespace and type checking. Combined with declarative make calls that define action order, the data flow through a service is visible at a glance:
class Payments::Process < ApplicationService::Base
input :payment, type: Payment
internal :charge_result, type: Servactory::Result
output :payment, type: Payment
make :validate_status!
make :perform_request!
make :handle_response!
make :assign_payment
private
def validate_status!
return if inputs.payment.pending?
fail!(
message: "Payment has already been processed",
meta: { status: inputs.payment.status }
)
end
def perform_request!
internals.charge_result = Gateway::Charge.call(
amount: inputs.payment.amount,
token: inputs.payment.token
)
end
def handle_response!
internals.charge_result
.on_success { handle_success! }
.on_failure { handle_failure! }
end
def handle_success!
inputs.payment.complete!(internals.charge_result.response.id)
end
def handle_failure!
inputs.payment.fail!(internals.charge_result.error.message)
fail_result!(internals.charge_result)
end
def assign_payment
outputs.payment = inputs.payment
end
end
What I think makes it worth trying:- Type safety on all three layers — inputs, internals, and outputs are each type-checked independently - Explicit data flow — the separation into three namespaces (inputs., internals., outputs.*) eliminates "where does this value come from?" confusion - Structured failure handling — fail! with metadata, typed failures, fail_result! for error propagation, on_success/on_failure hooks on the result - Action grouping — stage blocks with wrap_in for transactions, only_if conditions, and rollback handlers - Full ecosystem — custom RuboCop cops, OpenTelemetry instrumentation, RSpec matchers, Rails generators
Supports Ruby 3.2+ and Rails 5.1 through 8.1. 110K+ gem downloads, 138 releases, MIT licensed. I've been using it in production for over two years.
Would love to hear feedback, especially from people who've tried other approaches (Interactor, ActiveInteraction, Trailblazer, or plain POROs).
Docs: https://servactory.com