Using a model class (even a simple PORO in app/models) for features has several advantages in Rails.
This is why 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 is placed 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).
Example:
Instead of:
if user.admin? && SomeConfig.beta_enabled?
You do:
if BetaAccess.allowed_for?(user)
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 has logic that may grow, models keep it in one place.
Example: onboarding flow:
class Onboarding
def completed?(user)
user.profile_filled? && user.verified? && user.tutorial_done?
end
end
If later you add new onboarding rules — you update one class.
5. Better naming + improved readability
A dedicated model communicates intent clearly:
if FeatureFlag.enabled?(:new_ui)
is more readable than:
if Rails.configuration.x.new_ui_enabled
or random constants.
6. Supports persistence easily later
You might start with:
class FeatureFlag
FLAGS = { new_ui: false }
end
Later decide to store flags in DB:
class FeatureFlag < ApplicationRecord
end
Same interface, no major changes in the rest of the app.
7. Integrates cleanly with Pundit / CanCan / services
A model class naturally fits into authorization and service layers.
Example:
class FeatureFlagPolicy < ApplicationPolicy
def enable?
user.admin?
end
end
This wouldn’t be as clean if the logic lived in a helper or initializer.
Why use model classes for features?
Using a model (ActiveRecord or PORO) gives you:
- Clean architecture
- Centralized logic
- Reusability
- Easier testing
- Readable code
- Extensibility if the feature grows
- Smooth integration with Pundit and other layers
What about Service Object?
You can use a service or other patterns.
A model class is not the only valid approach.
The reason some teams choose a model instead of a service depends on what kind of logic they are modeling.
Here’s a clear breakdown.
Why not a service? (When a service is the wrong fit)
1. Services are for actions, not *state*
A service object usually represents something the system does:
SendEmail.new.callImportCsv.new.callCreateSubscription.new.call
Services are “verb-like.”
Feature flags (or similar domain rules) are not actions.
They’re state + rules about state.
Example:
FeatureFlag.enabled?(:new_ui)
A service would feel unnatural:
FeatureFlagChecker.new(:new_ui).call
You’re not doing anything; you're querying domain state.
2. Services don’t naturally hold reusable domain logic
If you put your logic in a service, you often end up with this anti-pattern:
- One service to check feature flags
- Another to read them
- Another to list them
- Another to validate something
- Another for authorization
The logic becomes scattered.
A model consolidates it.
3. Services can’t be easily plugged into Rails tools
Rails gives you conveniences for models:
- Cleaner testing
- Namespacing (
app/models) - Integration with Pundit
- Lazy loading / autoload
- DB migrations if you later add persistence
Services don’t get these benefits.
Why a model is often the better fit
1. It represents a *domain concept*
Feature flags, onboarding rules, eligibility rules, user states —
these aren’t actions.
These are entities in your domain.
Rails convention: entities → model classes.
Example domain models that aren’t ActiveRecord:
AuthenticatorCartCheckoutOnboardingSubscriptionRulesFeatureFlag
Rails devs often keep PORO domain models in app/models.
2. Models provide a clear, consistent API
Models give you intuitive, domain-driven APIs:
FeatureFlag.enabled?(:new_ui)
Eligibility.for(user).allowed?
Onboarding.completed?(user)
PlanPrice.for(:premium)
Services… not so much:
FeatureFlagService.new(:new_ui).enabled?
EligibilityService.new(user).allowed?
OnboardingService.new(user).completed?
This is more verbose, less “domain-sounding.”
3. Models grow better over time
Feature flags start simple but later evolve:
- per-user access
- groups / roles
- rollout percentages
- time-based enabling
- database storage
- admin UI
- audit logs
Models evolve cleanly.
Services become spaghetti when they accumulate state + logic.
So when would a service be better?
A service is appropriate if:
- The feature is about executing a task
- Something that has a start and end
- It produces a result or side effect
Examples:
GenerateReportChargeCreditCardSendWelcomeEmail
Not good for:
- feature flags
- eligibility checks
- rules
- state machines
- business constraints
Those map better to models.
What is an Interactor?
In Rails (especially with gems like ActiveInteractor or Interactor), an Interactor is a pattern for encapsulating a single unit of business logic — usually a transactional action that may:
- Take input
- Perform a multi-step operation
- Return a result (success/failure)
- Handle errors cleanly
Think of Interactors as “coordinators of actions”, often orchestrating multiple models and services.
Example:
class CreateOrder
include Interactor
def call
order = Order.create!(context.params)
PaymentProcessor.charge(order)
NotificationMailer.order_created(order).deliver_later
context.order = order
rescue StandardError => e
context.fail!(error: e.message)
end
end
Models vs Services vs Interactors
| Concept | Responsibility | Examples |
|---|---|---|
| Model | Represents domain state & rules; encapsulates attributes & behavior |
FeatureFlag, User, Subscription
|
| Service | Performs a discrete action; can use multiple models |
PaymentProcessor, EmailSender
|
| Interactor | Orchestrates a workflow or transaction using models & services; handles success/failure |
CreateOrder, 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 (one feature like a door or wheel)
- Interactor = the full Lego set (puts multiple creations together into a working system)
When to use Interactors vs Service vs Model
- Use a Model: for state, rules, calculations, or queries
- Use a Service: for single actions that act on models
- Use an Interactor: for multi-step workflows that may fail and need clean orchestration
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
| Pattern | Best for | Bad for |
|---|---|---|
| Model (PORO or ActiveRecord) | Domain concepts, rules, states | One-time actions |
| Service | Executable actions (“do X”) | Representing domain objects |
| Initializer / config | Static rules | Rules that may grow or need dependencies |
| Interactor | Orchestrating multi-step workflows / transactions | Single-purpose state or simple rules |
Originally posted at DevBlog.
Top comments (0)