Using Ruby Model Classes, Service Objects and Interactors

Published: (January 15, 2026 at 04:47 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Why Use a Model Class for Features?

Using a model class (even a simple PORO in app/models) for features has several advantages in Rails.
Developers often prefer “a model class” over scattering logic in controllers, helpers, or initializers.

Below are the key advantages:

1. Centralized, reusable logic

If a feature’s logic lives in a model class (e.g., FeatureFlag, BetaAccess, Onboarding), you can reuse it from:

  • Controllers
  • Views
  • Background jobs
  • Services
  • Pundit policies

…instead of duplicating the logic in multiple places.

2. Keeps controllers and views clean

Rails controllers and views should stay thin.
Putting domain logic in a model keeps your design clean (Fat Model, Skinny Controller).

Instead of:

if user.admin? && SomeConfig.beta_enabled?
  # …
end

Do:

if BetaAccess.allowed_for?(user)
  # …
end

3. Better testability

Models are the easiest to test:

RSpec.describe BetaAccess do
  describe ".allowed_for?" do
    # …
  end
end

No need to spin up controllers or simulate web requests.

4. Encapsulation of rules

If your feature’s logic may grow, a model keeps it in one place.

class Onboarding
  def completed?(user)
    user.profile_filled? && user.verified? && user.tutorial_done?
  end
end

Add new onboarding rules later – just update the class.

5. Better naming + improved readability

A dedicated model communicates intent clearly:

if FeatureFlag.enabled?(:new_ui)
  # …
end

…is more readable than:

if Rails.configuration.x.new_ui_enabled
  # …
end

6. Supports persistence easily later

You might start with a simple PORO:

class FeatureFlag
  FLAGS = { new_ui: false }
end

Later you can switch to an ActiveRecord model without changing the public interface:

class FeatureFlag  e
    context.fail!(error: e.message)
  end
end

Models vs. Services vs. Interactors

ConceptResponsibilityExamples
ModelRepresents domain state & rules; encapsulates attributes & behaviorFeatureFlag, User, Subscription
ServicePerforms a discrete action; can use multiple modelsPaymentProcessor, EmailSender
InteractorOrchestrates a workflow/transaction using models & services; handles success/failureCreateOrder, SendWeeklyReport, EnrollUserInCourse

Key difference

  • Services = do one thing.
  • Interactors = orchestrate multiple things as a single business operation.

How Interactors Fit Between Models and Services

  • Models → hold state and domain logic

    FeatureFlag.enabled?(:new_ui)
  • Services → perform actions related to one model or domain

    PaymentProcessor.charge(order)
  • Interactors → coordinate multiple models and services into a single, transactional workflow

    CreateOrder.call(params: order_params)

Analogy

  • Model = the Lego bricks
  • Service = a single Lego creation (e.g., a door or wheel)
  • Interactor = the full Lego set (puts multiple creations together into a working system)

When to Use Interactors vs. Services vs. Models

Use caseRecommended pattern
State, rules, calculations, or queriesModel
Single actions that act on modelsService
Multi‑step workflows that may fail and need clean orchestrationInteractor

Example Workflow

# Model
class User; end
class FeatureFlag; end

# Service
class WelcomeEmailSender; end

# Interactor
class OnboardNewUser
  include Interactor

  def call
    user = User.create!(context.params)
    WelcomeEmailSender.send(user)
    context.success_message = "Welcome #{user.name}!"
  rescue => e
    context.fail!(error: e.message)
  end
end

Summary

PatternBest forBad for
Model (PORO or ActiveRecord)Domain concepts, rules, statesOne‑time actions
ServiceExecutable actions (“do X”)Representing domain objects
Initializer / configStatic rulesRules that may grow or need dependencies
InteractorOrchestrating multi‑step workflows / transactionsSingle‑purpose state or simple rules

Originally posted at DevBlog.

Back to Blog

Related posts

Read more »

Released Gon v7.0.0

Release v7.0.0 Gon v7.0.0 releasehttps://github.com/gazay/gon/releases/tag/v7.0.0 – this major version bump introduces breaking changes. Breaking change: reque...

ZJIT is now available in Ruby 4.0

Article URL: https://railsatscale.com/2025-12-24-launch-zjit/ Comments URL: https://news.ycombinator.com/item?id=46393906 Points: 21 Comments: 9...