RubyFlow The Ruby and Rails community linklog

×

The Ruby and Rails community linklog

Made a library? Written a blog post? Found a useful tutorial? Share it with the Ruby community here or just enjoy what everyone else has found!

Servactory – Typed service objects with declarative actions for Ruby

Ruby’s flexibility is great until every developer on the team writes service objects differently. After years of working with Rails codebases where each service had its own structure, error handling, and testing conventions, I built Servactory — a framework that standardizes service objects through a typed, declarative DSL.

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

Post a comment

You can use basic HTML markup (e.g. <a>) or Markdown.

As you are not logged in, you will be
directed via GitHub to signup or sign in