What is MVC (Model-View-Controller) in Rails?
🧠 Detailed Explanation
MVC stands for Model-View-Controller. It is a way to organize your Rails app in 3 clear parts:
- Model: Think of this as the part that talks to the database. It holds the data and business logic. Example:
Article
,User
. - View: This is what the user sees. It’s the HTML, CSS, and embedded Ruby (ERB) code that shows the page in the browser.
- Controller: This is the middleman. It takes user input (like clicking a link), asks the Model for data, and passes it to the View.
Let’s imagine a **real-world example**:
- You visit
/articles
in your browser. - The Controller (ArticlesController) gets this request.
- It asks the Model (Article) to get all articles from the database.
- Then it sends that data to the View (
index.html.erb
). - The View shows all the articles on your screen.
Think of it like a restaurant:
- Model = Kitchen (where food is prepared)
- Controller = Waiter (takes your order and brings your food)
- View = Plate/Table (how food is served and what you see)
This way of organizing code helps developers keep everything clean, easy to manage, and work in teams without confusion.
📘 Key MVC Terms & Best Practices in Rails
🔖 Term / Practice | 📄 Description |
---|---|
Model | Handles data, business logic, and database interaction using ActiveRecord. |
View | Renders user-facing HTML templates using ERB or other engines. |
Controller | Receives requests, communicates with models, and renders views or redirects. |
Routes | Defines URL patterns that map to controller actions (in routes.rb ). |
Strong Parameters | Ensures only permitted attributes are passed to models — avoids mass assignment. |
Partials | Reusable HTML/ERB snippets to avoid repetition in views (e.g., forms). |
Flash Messages | Used to show user feedback like success or error messages. |
Before Action | A controller filter that runs before specific actions (e.g., set resources). |
Helper Methods | UI-specific logic or formatters used inside views (e.g., date formatting). |
Service Objects | Encapsulate complex business logic outside models and controllers. |
Concerns | Modules used to share reusable logic between models or controllers. |
Scopes | Custom query methods defined in models for cleaner ActiveRecord queries. |
Validations | Model-level checks to ensure data is valid before saving to DB. |
DRY Principle | “Don’t Repeat Yourself” — reuse views, helpers, and logic to stay efficient. |
RESTful Routing | Follow conventions like index , show , create , update , and destroy . |
Render vs Redirect | render displays a view. redirect_to sends the user to a new URL. |
Application Layout | Default layout template for consistent headers/footers across pages. |
ActiveRecord | Rails’ ORM system for interacting with the database using Ruby. |
Rendering JSON | Return API-style responses using render json: data in controller. |
Migrations | Version-controlled schema changes (add/remove/update DB fields). |
Logging | Use Rails.logger for debugging without printing in views. |
Testing (RSpec/Minitest) | Write unit, request, and system tests to ensure correctness and stability. |
🌐 MVC Flow Cycle (Step-by-Step)
Here’s how a request flows through a typical Rails application using the MVC pattern:
- 🔗 User Action: A user visits a URL in the browser (e.g.,
/articles
). - 🌐 Router (config/routes.rb): Rails checks this file to find out which controller and action should handle the request.
- 🧭 Controller: The matched controller action is called (e.g.,
ArticlesController#index
). This acts as the traffic manager. - 🧠 Model: If needed, the controller asks the model to fetch or save data from the database using ActiveRecord (e.g.,
Article.all
). - 👀 View: The controller sends that data to the view template (e.g.,
index.html.erb
), which generates HTML to show in the browser. - 📲 Response: Rails sends the final HTML back to the user’s browser. The user sees the result.
Summary Flow:
User → Router → Controller → Model → Controller → View → Response
Example: Visiting /articles
in the browser
routes.rb
: maps toarticles#index
ArticlesController
: loads@articles = Article.all
index.html.erb
: loops through@articles
and displays them- Browser: shows the page with all articles
Think of it like a restaurant:
- You (User): place an order (URL request)
- Waiter (Controller): takes your order
- Kitchen (Model): prepares your food (data)
- Plate (View): presents the food
- Back to You (Browser): you eat it (see the result)
💡 Examples
# app/models/article.rb
class Article < ApplicationRecord
validates :title, presence: true
end
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
end
# app/views/articles/index.html.erb
<% @articles.each do |article| %>
<h2><%= article.title %></h2>
<% end %>
🔁 Alternative Concepts
- MVVM (Model-View-ViewModel)
- HMVC (Hierarchical MVC)
- Component-based architecture (used in React)
🛠️ Technical Questions & Answers
Q1: How do you define a model in Rails?
A: Use a class that inherits from ApplicationRecord
. It represents a database table.
class Article < ApplicationRecord
validates :title, presence: true
end
Q2: What is a controller in Rails?
A: A controller receives requests from the browser and decides what to do — like fetching data and rendering a view.
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
end
Q3: How do you create a route in Rails?
A: You define routes in config/routes.rb
file.
get '/articles', to: 'articles#index'
Q4: How does Rails connect controller to views?
A: If a controller action is called, Rails automatically renders a view with the same name.
# Controller
def show
@article = Article.find(params[:id])
end
# View: app/views/articles/show.html.erb
Q5: What is the naming convention in MVC?
A: Models are singular (Article), Controllers are plural (ArticlesController), and Views match controller name (articles/index).
Q6: How do you pass data from controller to view?
A: Use instance variables (e.g., @article
).
# Controller
@article = Article.find(1)
# View
<%= @article.title %>
Q7: How do you fetch form input in controller?
A: Use params[:field_name]
.
def create
title = params[:title]
end
Q8: How do you define validations in models?
A: Use validates
keyword.
validates :title, presence: true, uniqueness: true
Q9: What is strong parameter in Rails?
A: It’s used to whitelist allowed params.
params.require(:article).permit(:title, :body)
Q10: How do you render partial views?
A: Use render
in view files.
<%= render 'form' %>
Q11: How do you handle 404 not found?
A: Use ActiveRecord::RecordNotFound
rescue.
def show
@article = Article.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to articles_path, alert: "Not found"
end
Q12: How do you redirect in controller?
A: Use redirect_to
method.
redirect_to articles_path, notice: "Article created!"
Q13: How do you render JSON from a controller?
A: Use render json:
.
render json: @article
Q14: What is DRY principle in MVC?
A: “Don’t Repeat Yourself” — reuse partials, helpers, and concerns.
Q15: How do you organize logic that doesn’t belong in controller or model?
A: Use service objects or concerns.
Q16: How do you use helper methods in views?
A: Define methods in app/helpers
.
module ArticlesHelper
def format_date(date)
date.strftime("%b %d, %Y")
end
end
Q17: What is the difference between render and redirect_to?
A: render
shows a template without a new request. redirect_to
sends a new request.
Q18: How do you share logic between multiple controllers?
A: Use controller concerns (mixins).
# app/controllers/concerns/auth_helper.rb
module AuthHelper
def current_user
User.find(session[:user_id])
end
end
Q19: How to debug inside a controller?
A: Use byebug
or Rails.logger.debug
.
Rails.logger.debug "Here is params: #{params.inspect}"
Q20: What is the default folder structure in MVC?
A:
app/models/
→ contains model filesapp/controllers/
→ controller logicapp/views/
→ HTML templates
✅ Best Practices
1. Keep Controllers Thin, Models Fat
Move most of the business logic to models or service objects. Controllers should only coordinate tasks.
# ❌ Bad: logic in controller
def create
user = User.new(params[:user])
if user.age >= 18
user.approved = true
end
user.save
end
# ✅ Good: move logic to model
# app/models/user.rb
def approve_if_adult
self.approved = true if age >= 18
end
# controller
def create
user = User.new(user_params)
user.approve_if_adult
user.save
end
2. Use Partial Views for Reusable HTML
Break your views into smaller components (called partials) to avoid repetition.
<%= render 'form' %>
# app/views/articles/_form.html.erb
<form>
...
</form>
3. Use Strong Parameters for Security
Always whitelist params to avoid mass-assignment attacks.
params.require(:article).permit(:title, :content)
4. Follow RESTful Routes
Use standard routes and actions like index
, show
, create
, update
, and destroy
.
resources :articles
5. DRY – Don’t Repeat Yourself
Use helpers, layouts, partials, and concerns to avoid writing the same code multiple times.
6. Validate Data in Models
Use built-in validations like presence
, length
, uniqueness
, etc.
validates :email, presence: true, uniqueness: true
7. Use Flash Messages for Feedback
Let users know what’s happening (success, error, etc.).
redirect_to articles_path, notice: "Article saved!"
8. Handle Errors Gracefully
Use rescue_from
or simple rescue
blocks to handle exceptions without crashing the app.
rescue ActiveRecord::RecordNotFound
redirect_to root_path, alert: "Not found."
9. Use Layouts for Consistent UI
Put common HTML (e.g., navbar, footer) in app/views/layouts/application.html.erb
.
10. Organize Files Properly
Keep your code clean. For example:
app/models/
– Business logicapp/controllers/
– Request handlingapp/views/
– Presentationapp/services/
– Extracted logic (optional)app/helpers/
– Reusable UI helpers
11. Write Tests
Use RSpec or Minitest to test models, controllers, and features.
# RSpec example
describe Article do
it "is valid with a title" do
expect(Article.new(title: "Test")).to be_valid
end
end
12. Use Scopes for Query Logic
# app/models/article.rb
scope :published, -> { where(published: true) }
13. Don’t Query in Views
Prepare all data in the controller.
# ❌ Bad
<% Comment.where(article_id: article.id).each do |comment| %>
# ✅ Good
@comments = article.comments
14. Use before_action Filters
before_action :set_article, only: [:show, :edit, :update]
15. Use Helpers in Views, Not Logic
Format dates, texts, etc. using helpers.
<%= format_date(@article.created_at) %>
🌍 Real-World Use Cases
- Blog platforms
- Project management tools
- E-commerce platforms
- CRM systems
What are Service Objects in Rails?
🧠 Detailed Explanation
Service Objects are plain Ruby classes that help us organize code when things get too messy in controllers or models.
In a Rails app, controllers are supposed to only handle **requests** and **responses**. Models are meant to deal with **data and validations**. But what if you need to do something more complex — like register a user, send a welcome email, and log the action?
That’s too much for one place — and that’s where a Service Object comes in!
It’s a class that does one job only, and does it well.
- 📦 It’s placed in the
app/services
folder - 📞 You usually call it like:
MyService.new(...).call
- ⚙️ Inside it, you can handle things like saving data, sending emails, calling APIs, etc.
Why use it?
- ✅ Keeps your controller short and clean
- ✅ Keeps your model focused on data
- ✅ Makes your code easier to read and test
Think of it like this:
- 🚕 Controller = Taxi driver (takes request)
- 🧾 Model = Address book (stores info)
- 🛠️ Service Object = GPS system (calculates best route and executes the task)
In short, **Service Objects help you write cleaner, organized, and reusable code** when logic becomes too complex for normal controllers or models.
📘 Key Terms & Best Concepts – Service Objects in Rails
🔖 Term / Concept | 📄 Description |
---|---|
Service Object | A plain Ruby class that performs a single, specific task. Used for complex business logic. |
Single Responsibility Principle | Each class should do only one thing. Service Objects follow this rule strictly. |
call method | The public method used to run the service logic. Conventionally named #call . |
app/services/ | Folder where Service Objects are typically stored in a Rails app. |
Plain Old Ruby Object (PORO) | A simple Ruby class not tied to Rails — ideal for service classes. |
Decoupling | Separating business logic from models/controllers, making the system easier to maintain and test. |
Composability | Service Objects can be chained or reused in other services or flows. |
Encapsulation | Hides internal logic and exposes only one method (call ) — keeping the interface simple. |
Input Parameters | Service objects are initialized with required inputs (like params or objects). |
Return Values | Service Objects should return meaningful values or results (like a created record or boolean). |
Error Handling | Rescue and handle errors gracefully within the service class to avoid controller bloat. |
Testability | Because service objects are small and independent, they’re very easy to write unit tests for. |
Naming Convention | Use verb-based names like RegisterUser , SendInvoice , or CreateReport . |
Alternative: Interactor | An alternative approach using the Interactor gem which offers context objects and rollback support. |
Alternative: Form Object | Used for forms that combine multiple models — sometimes overlaps with service object behavior. |
Alternative: Command Pattern | Service objects follow the command pattern — one object = one command (action). |
Don’t Access Params Directly | Extract needed values in the controller and pass them into the service. Keep services pure. |
Avoid Side Effects | Unless expected (e.g., sending email), avoid modifying global state or session inside services. |
Logging | Use Rails.logger to log steps inside a service if debugging complex workflows. |
Reusability | You can call the same service from multiple controllers or other services. |
🔁 Service Object Flow – Step by Step
- 🧑💻 Controller receives a request
User submits a form or sends an API call. - 📦 Controller creates a service object instance
The controller passes the required data to the service.service = MyService.new(params)
- 🚀 Controller calls the service
The controller runs the service’scall
method.
result = service.call
- ⚙️ Service runs business logic
Service performs tasks like saving data, sending email, etc. - 📲 Controller handles result
Based on the return value, the controller decides what to render or where to redirect.
✅ Clean flow:
Controller → Service Object → Business Logic → Return Result → Controller Response
📌 Where & How to Use Service Objects
Use Service Objects anytime your controller or model is doing too much. Here are the most common use cases:
📍 Use Case | 💡 Description |
---|---|
🧾 User Registration | Save user, send welcome email, log event, assign default roles — all in one service. |
💳 Payment Processing | Call a payment gateway, record transaction, send invoice — handled via service. |
📬 Sending Emails | For custom emails like password resets, notifications, or marketing. |
📊 Generating Reports | Service objects can generate PDFs or data exports from filtered records. |
🎫 Booking & Checkout | Booking systems or e-commerce checkout flows can be neatly encapsulated. |
🔄 Background Jobs | Use services within Sidekiq jobs to keep business logic reusable and testable. |
🔐 API Integration | Calling external APIs (e.g., SMS, Stripe, Firebase) is best done in a service. |
🔗 Multi-step Operations | If multiple models or workflows need to happen together (e.g., create order + customer), use a service. |
🧠 Tip: If you’re writing “if this, then that, then this again…” in a controller — it’s time to use a Service Object.
🛠️ Best Implementation (Step-by-Step)
This example shows how to use a Service Object to register a user, send a welcome email, and log the activity — keeping the controller clean and logic reusable.
📁 1. Create a folder for services
If it doesn’t already exist, create a new folder in your Rails app:
mkdir app/services
📦 2. Create the Service Object
File: app/services/user_registration_service.rb
class UserRegistrationService
def initialize(user_params)
@user_params = user_params
end
def call
user = User.new(@user_params)
if user.save
send_welcome_email(user)
log_registration(user)
end
user
end
private
def send_welcome_email(user)
UserMailer.welcome_email(user).deliver_later
end
def log_registration(user)
Rails.logger.info("User #{user.email} registered at #{Time.now}")
end
end
- ✅ Uses
initialize
to receive data - ✅ Public
call
method performs the main task - ✅ Private methods encapsulate side tasks (email, log)
🎮 3. Use in Controller
File: app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = UserRegistrationService.new(user_params).call
if @user.persisted?
redirect_to @user, notice: "Welcome!"
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
end
✅ Clean controller: All business logic is moved to the service.
📧 4. Create a Mailer (Optional)
If you don’t already have a mailer:
rails generate mailer UserMailer welcome_email
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome_email(user)
@user = user
mail(to: @user.email, subject: "Welcome to the platform!")
end
end
🧪 5. Add a Simple Test
# spec/services/user_registration_service_spec.rb
require 'rails_helper'
RSpec.describe UserRegistrationService do
it "creates a user and sends email" do
params = { name: "Ali", email: "ali@example.com", password: "password" }
service = described_class.new(params)
user = service.call
expect(user).to be_persisted
end
end
📝 Summary
- 📍 Create a class in
app/services
- 🧼 Keep only one public method:
call
- ⚙️ Move logic out of controller
- ✅ Test independently like any Ruby class
🎯 Result: Cleaner code, easier testing, reusable logic, and proper Rails architecture.
💡 Examples
# app/services/user_registration_service.rb
class UserRegistrationService
def initialize(user_params)
@user_params = user_params
end
def call
user = User.new(@user_params)
if user.save
UserMailer.welcome_email(user).deliver_later
end
user
end
end
# controller
def create
@user = UserRegistrationService.new(user_params).call
end
🧩 Gems & Libraries for Service Objects
🔧 Gem / Library | 📄 Description | ✅ Use Case |
---|---|---|
Interactor | Provides a clean, structured interface for service-like classes with .call and context objects. | Multi-step processes, rollback, organized business logic |
dry-monads | Functional programming tools including result objects, success/failure patterns, and chaining. | When you want error-safe flow, composability, and functional purity |
dry-transaction | Helps build complex workflows by chaining service steps using a transaction-style DSL. | Service pipelines, advanced business logic flows |
Trailblazer::Operation | Encapsulates business logic in operations with validations, steps, and error handling. | Full-service architecture with validations and policy control |
ServicePattern | A tiny gem that provides a base class for simple service objects with a .call interface. | Basic service object support, very lightweight |
ActiveInteraction | Combines inputs, validations, and business logic into one unit — great for form-like logic. | Input-heavy services, form-style objects |
Wisper | Implements the pub/sub pattern, allowing services to broadcast events to listeners. | When you want to decouple side effects like emails or logging |
🔁 Alternative Concepts
- Fat Models (not recommended for complex logic)
- Command Pattern
- Interactors (e.g.,
Interactor
gem) - Observers (for background triggers)
🛠️ Technical Questions & Answers
Q1: What is a Service Object in Rails?
A: A Service Object is a plain Ruby class that performs a single job, like processing a payment or registering a user.
# app/services/charge_payment.rb
class ChargePayment
def initialize(order)
@order = order
end
def call
PaymentGateway.charge(@order.amount)
end
end
Q2: What is the recommended method name in service objects?
A: Use a public method named call
to keep consistency across services.
def call
# perform action
end
Q3: How do you use a service object in a controller?
A: Instantiate and call it with parameters.
@result = MyService.new(params).call
Q4: Where do you store service objects in a Rails app?
A: Inside the app/services
directory.
Q5: How do you return errors from a service object?
A: Use a result object or add an @errors
instance variable.
def call
if some_failure?
@errors = ["Something went wrong"]
return false
end
true
end
Q6: Can service objects trigger ActiveJob or mailers?
A: Yes, they are perfect for wrapping background jobs or email tasks.
def call
UserMailer.welcome_email(user).deliver_later
end
Q7: How do you test a service object?
A: Use a plain RSpec test or Minitest unit test.
# RSpec
describe RegisterUser do
it "creates user" do
user = RegisterUser.new(params).call
expect(user).to be_persisted
end
end
Q8: What are alternatives to service objects in Rails?
A: You can use:
Interactor
gem- Form Objects
- Command Pattern manually
Q9: Can you return a model from a service?
A: Yes. Most services return a model, boolean, or response object.
def call
user = User.create(@params)
return user
end
Q10: Can service objects use transactions?
A: Yes! Wrap related database calls inside ActiveRecord::Base.transaction
.
def call
ActiveRecord::Base.transaction do
user.save!
profile.save!
end
end
Q11: Should you include Rails helpers or sessions in a service?
A: No. Keep services pure. Don’t use controller-specific helpers or global state.
Q12: Can service objects raise errors?
A: Yes, but it’s better to handle errors gracefully and return a result or error message.
Q13: Can you inject dependencies in a service object?
A: Yes, you can inject collaborators through the constructor.
def initialize(order, payment_gateway = Stripe)
@order = order
@gateway = payment_gateway
end
Q14: How do you chain service objects?
A: One service can call another service from within its call
method.
def call
result = CreateOrder.new(order_params).call
SendInvoice.new(result).call
end
Q15: Can you memoize values inside a service?
A: Yes, use instance variables.
def user
@user ||= User.find(@user_id)
end
✅ Best Practices with Examples
1. Name Service Objects with Verbs
Use names that describe the action being performed (e.g., CreateInvoice
, RegisterUser
, SendEmail
).
# Good
RegisterUser.new(user_params).call
2. Put Services in app/services/
This keeps your app organized and follows Rails conventions.
app/
services/
register_user.rb
send_invoice.rb
3. Use a call
Method
Always define a single public call
method — this keeps usage consistent and readable.
def call
# main logic
end
4. Keep the Service Focused (Single Responsibility)
Each service should do only one thing. Don’t mix tasks.
# ✅ Good: One responsibility
class RegisterUser
def call
create_user
send_welcome_email
end
end
5. Return a Value (Model, Boolean, Result Object)
This makes the service predictable and testable.
def call
user = User.new(...)
user.save
user
end
6. Use Private Methods for Subtasks
Keep the call
method short by splitting complex steps into private methods.
def call
create_user
notify_user
end
private
def create_user
...
end
7. Don’t Use Controller Stuff (params, session)
Extract what you need in the controller, then pass it in. Keep services pure Ruby.
# Controller
RegisterUser.new(user_params).call
8. Use Transactions if Multiple DB Writes
Prevent partial saves by wrapping logic inside a transaction.
def call
ActiveRecord::Base.transaction do
user.save!
profile.save!
end
end
9. Handle Errors Inside the Service
Don’t leak logic errors to the controller — return false, a message, or raise a handled error.
def call
return false unless valid_input?
do_something
end
10. Write Tests for Each Service
Since services are plain Ruby classes, they are easy to test with RSpec or Minitest.
describe SendWelcomeEmail do
it "delivers the email" do
result = SendWelcomeEmail.new(user).call
expect(result).to be_truthy
end
end
🌍 Real-World Use Cases
- 📬 Sending confirmation or welcome emails
- 💳 Handling third-party payment gateway logic
- 📊 Generating downloadable reports or exports
- 📦 Placing an order and charging the customer
- 🎟️ Ticket booking workflows
What are Form Objects in Rails?
🧠 Detailed Explanation
Form Objects are Ruby classes that help you manage form input when:
- ✅ You need to use one form to save/update **multiple models**
- ✅ You want to validate form data **before** saving it
- ✅ You don’t want to bloat your **controller or models** with form-specific logic
Rails models (like User
) are great when your form matches one table. But what if you have a signup form that:
- Collects user name and email (User model)
- Sends a welcome email (Mailer)
- Logs the action (Logger)
That’s too much for one model or controller! 🎯 This is where a Form Object shines.
It acts like a model but doesn’t save to the database directly. Instead, it:
- Collects the form data
- Validates it
- Then saves or updates the real models inside its
save
method
How?
class SignupForm
include ActiveModel::Model
attr_accessor :name, :email, :password
validates :name, :email, :password, presence: true
def save
return false unless valid?
User.create(name: name, email: email, password: password)
end
end
Why it’s useful:
- 🧼 Keeps controller actions short
- 📦 Keeps models focused on data only
- 🔍 Easy to test like any Ruby class
Think of it like:
- 🧾 A translator between your form and your models
- 🛠 A one-stop tool that collects, checks, and passes on the data
Bonus: Form objects also make writing multi-step forms or wizard-style pages way easier!
🔁 How Form Objects Work (Step-by-Step Flow)
- 👤 User submits a form
For example, a signup or checkout form in your app. - 📦 Controller initializes a form object
The controller passes form values to a class likeSignupForm.new(params)
. - ✅ Form object validates the input
It usesActiveModel::Validations
to check if everything is okay. - 💾 Form object runs logic inside
save
If the data is valid, it creates or updates one or more models, sends an email, etc. - 📲 Controller redirects or renders based on result
If successful: redirect to dashboard. If failed: render the form with errors.
Typical Flow:
Form Submit → Controller → Form Object → Validate + Save → Return Result
📌 Where & How to Use Form Objects
Form Objects are best when your form is doing more than just saving one table (model). Here’s when and where to use them:
📍 Use Case | 💡 Description |
---|---|
👤 User Signup | Handles user creation, email confirmation, welcome message, and more. |
👥 Profile Update | Updates multiple models like User and Address in a single form. |
🛒 Checkout Form | Combines user, shipping address, order, and payment into one unified form. |
📋 Multi-Step Wizard | For forms split across multiple pages (e.g., job applications, surveys). |
🔒 Account Recovery | Collects an email and verification code and resets a password safely. |
📊 Filter/Search Forms | Use a form object to handle form inputs for filters or search queries (not saved to DB). |
🧠 Rule of Thumb:
If your form:
- Touches more than one model, OR
- Is not tied directly to a database table
➡️ Then it’s a good candidate for a Form Object.
📘 Key Terms & Concepts – Form Objects in Rails
🔖 Term / Concept | 📄 Description |
---|---|
Form Object | A plain Ruby class that handles form data and validations without tying directly to a database table. |
ActiveModel::Model | A module that gives plain Ruby classes model-like behavior (validations, forms compatibility). |
attr_accessor | Used in form objects to define form fields and give them getter/setter behavior. |
validates | Defines validations inside the form object, just like in ActiveRecord models. |
save method | The custom method in a form object where you control what happens when the form is submitted. |
Multiple Models | Form objects can create/update multiple models in one flow (e.g., User + Address). |
app/forms/ | A custom directory used to organize form objects in your Rails app. |
Wizard Form | A multi-step form flow where a form object helps track data across steps and validate each stage. |
Form Model Separation | Keeps form-specific logic out of database models to follow Single Responsibility Principle (SRP). |
Testing Simplicity | Form objects are easy to unit test because they are plain Ruby classes with no DB dependency. |
form_with model: | You can pass a form object to Rails forms and use it like a real model because of ActiveModel. |
Decoupled Validation | All input validation logic is moved out of models/controllers and into form-specific classes. |
Pure Ruby | Form objects don’t need to inherit from ActiveRecord — they work with plain Ruby + ActiveModel. |
Reform Gem (optional) | A gem that helps build advanced form objects with a DSL and nested forms support. |
Command + Validation Pattern | Form objects combine validation + form actions into a single object that’s easier to reason about. |
Encapsulation | Form objects keep all logic related to a specific form inside one class. |
Single Responsibility Principle (SRP) | Each form object does one thing: handle the form logic, nothing else. |
🧩 Gems & Libraries for Form Objects in Rails
🔧 Gem / Library | 📄 Description | ✅ Use Case |
---|---|---|
ActiveModel (built-in) | Rails module that gives plain Ruby objects model-like features such as validations, naming, and form compatibility. | Core requirement for all form objects. Enables form_with model: . |
Reform | A full-featured form object gem with validations, compositions, coercion, and nested model support. | Complex forms, nested forms, wizard flows, dry-validations |
Virtus (deprecated but still used) | Attribute handling gem that works well with form objects, often used with Reform in older apps. | Optional support for form attributes (use dry-struct in new apps instead) |
dry-validation | A robust validation library with schema-based rules. Often used under the hood by Reform . | Strict data validation and coercion in form objects |
ActiveInteraction | Lets you combine form input, validation, and business logic in one class. | Reusable, parameter-heavy forms with clean service logic |
SimpleForm | Form builder gem that supports form objects via ActiveModel compatibility. | Rich form UI with custom components and input types |
FormObject (gem) | A tiny gem that wraps common form object logic, providing a base class and helpers. | Quickly set up simple form objects with fewer lines |
🛠️ Best Implementation – Full Walkthrough
This example demonstrates how to build a Signup Form using a Form Object in a Rails app. The form collects user details and handles validation, user creation, and welcome email delivery — cleanly separated from the controller.
📦 Step 1: Create a Form Object
File: app/forms/signup_form.rb
class SignupForm
include ActiveModel::Model
attr_accessor :name, :email, :password
validates :name, :email, :password, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
def save
return false unless valid?
user = User.create(
name: name,
email: email,
password: password
)
send_welcome_email(user)
user
end
private
def send_welcome_email(user)
UserMailer.welcome_email(user).deliver_later
end
end
- ✅ Uses
ActiveModel::Model
to behave like a Rails model - 🧪 Validations ensure the form data is correct before creating the user
- 🔧 Custom logic inside
save
creates the user and sends email
🎮 Step 2: Use It in a Controller
File: app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
def new
@form = SignupForm.new
end
def create
@form = SignupForm.new(signup_params)
if @form.save
redirect_to dashboard_path, notice: "Welcome!"
else
render :new
end
end
private
def signup_params
params.require(:signup_form).permit(:name, :email, :password)
end
end
- 🧼 Clean controller: business logic is inside the form object
- 🔥 Uses the form object like a model (
form_with model: @form
)
📝 Step 3: Build the Form in the View
File: app/views/registrations/new.html.erb
<%= form_with model: @form, url: registrations_path, local: true do |f| %>
<div>
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<div>
<%= f.label :email %>
<%= f.email_field :email %>
</div>
<div>
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<%= f.submit "Sign Up" %>
<% end %>
✅ Looks and behaves like a model-backed form even though it’s a plain Ruby class.
🧪 Step 4: Test the Form Object (RSpec)
File: spec/forms/signup_form_spec.rb
require 'rails_helper'
RSpec.describe SignupForm, type: :model do
it "is valid with valid data" do
form = SignupForm.new(name: "Ali", email: "ali@example.com", password: "password")
expect(form.valid?).to be true
end
it "is invalid without a name" do
form = SignupForm.new(email: "test@example.com", password: "pass")
expect(form.valid?).to be false
end
it "saves and creates a user" do
form = SignupForm.new(name: "Ali", email: "ali@example.com", password: "password")
user = form.save
expect(user).to be_a(User)
expect(user.persisted?).to be true
end
end
📝 Summary
- 📁 Organize form logic inside
app/forms
- 📦 Use
ActiveModel::Model
to gain validations and model-like behavior - 🧠 Define a single
save
method for all logic (validation + model updates) - 🎯 Keeps controller and model clean and focused
- 🧪 Easy to test — just like a regular Ruby class
💡 Examples
File: app/forms/signup_form.rb
class SignupForm
include ActiveModel::Model
attr_accessor :name, :email, :password
validates :name, :email, :password, presence: true
def save
return false unless valid?
user = User.create(name: name, email: email, password: password)
WelcomeMailer.send_email(user).deliver_later
end
end
Controller usage:
def create
@form = SignupForm.new(signup_params)
if @form.save
redirect_to dashboard_path
else
render :new
end
end
🔁 Alternative Concepts
- Using only ActiveRecord models (not good for complex forms)
- Service Objects for non-form related logic
- Reform gem (provides a DSL for form objects)
🛠️ Technical Q&A with Examples – Form Objects in Rails
Q1: What is a Form Object in Rails?
A: A Form Object is a plain Ruby class that handles form input, validations, and saves one or more models.
class ContactForm
include ActiveModel::Model
attr_accessor :name, :email
validates :name, :email, presence: true
end
Q2: Why use Form Objects instead of ActiveRecord models?
A: Use them when your form:
- Is not directly tied to one table
- Needs to update/create multiple models
- Has validations but no persistence
Q3: How do you validate data in a Form Object?
A: Use ActiveModel::Validations
(comes with ActiveModel::Model
).
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
Q4: How do you trigger logic after validation in a Form Object?
A: Add a custom save
method and call it from the controller.
def save
return false unless valid?
User.create(name: name, email: email)
end
Q5: Where should Form Objects be placed in a Rails app?
A: Best practice is to place them in app/forms/
.
Q6: Can I use form_with model:
with a Form Object?
A: Yes, if the object includes ActiveModel::Model
and defines persisted?
(even returning false).
def persisted?
false
end
Q7: How do I save multiple models inside a Form Object?
A: Just write custom logic in save
to update or create each model.
def save
return false unless valid?
user = User.create!(email: email)
profile = Profile.create!(user: user, bio: bio)
end
Q8: How do I test a Form Object?
A: You write unit tests like any Ruby class — no need to touch the database unless models are used.
describe SignupForm do
it "is valid with name and email" do
form = SignupForm.new(name: "Ali", email: "ali@example.com")
expect(form.valid?).to be true
end
end
Q9: Can I reuse form logic in multiple controllers?
A: Yes! That’s one of the key benefits — form objects are portable and reusable.
Q10: How do I show validation errors from a Form Object?
A: Same as regular models. Use @form.errors.full_messages
in the view.
<% @form.errors.full_messages.each do |msg| %>
<div class="error"><%= msg %></div>
<% end %>
✅ Best Practices with Examples
1. Use ActiveModel::Model
to behave like a Rails model
This gives your Form Object access to validations, form compatibility, and error handling.
class ContactForm
include ActiveModel::Model
attr_accessor :name, :email
validates :name, :email, presence: true
end
2. Place form objects in app/forms/
Keep your app organized by following a consistent structure.
app/
forms/
signup_form.rb
contact_form.rb
3. Use attr_accessor
to define form fields
This simulates model attributes and makes form binding work.
attr_accessor :email, :message
4. Define a save
method to control what happens on submission
Keep validations and logic separate from the controller.
def save
return false unless valid?
Message.create(email: email, body: message)
end
5. Use form_with model:
for easy view integration
Form objects work just like models in forms.
<%= form_with model: @form, url: messages_path do |f| %>
<%= f.text_field :email %>
<%= f.text_area :message %>
<% end %>
6. Avoid database logic in the controller
Let the form object handle validations and model persistence.
# Controller
@form = ContactForm.new(params[:contact_form])
if @form.save
redirect_to root_path
else
render :new
end
7. Use persisted?
to avoid errors in views
If you’re not using ActiveRecord, return false
.
def persisted?
false
end
8. Combine multiple models in the save
method
Handle user + profile creation in one form.
def save
return false unless valid?
user = User.create!(email: email)
Profile.create!(user: user, bio: bio)
end
9. Write unit tests for form objects
Since they don’t depend on the database, they’re easy to test.
describe ContactForm do
it "is invalid without an email" do
form = ContactForm.new(message: "Hello")
expect(form.valid?).to be false
end
end
10. Don’t overload with business logic
Form objects are for validation and model coordination — not for large-scale workflows. Use a service object for those.
🌍 Real-World Use Cases
- 📝 Signup forms that require creating a user and sending an email
- 👥 Profile forms that update both user and address models
- 📋 Multi-step wizards (like job applications or onboarding)
- 🛒 Checkout forms that handle orders, payment info, and address
What are Query Objects in Rails?
🧠 Detailed Explanation
Query Objects are plain Ruby classes that hold your complex database queries in one place — instead of stuffing them inside controllers or models.
They help you answer questions like:
- 🔍 “Who are our most active users this month?”
- 📦 “What orders were delayed last week?”
- 📊 “What are the top 10 products by sales in the last 30 days?”
Instead of writing this logic in the model or repeating it everywhere, you create a query class that does it once and cleanly.
Think of it like:
- 🧠 A smart question that knows how to get the answer from your database
- 📁 A reusable search tool you can call anytime
- 🧼 A clean way to move complicated ActiveRecord chains out of your models
Here’s a simple example:
# app/queries/active_users_query.rb
class ActiveUsersQuery
def initialize(days = 30)
@days = days
end
def call
User.where(active: true)
.where("last_login_at >= ?", @days.days.ago)
end
end
How to use it:
active_users = ActiveUsersQuery.new(7).call
Why it’s helpful:
- ✅ Reuse the same query in multiple places
- ✅ Easier to test — no Rails controller or view needed
- ✅ Keeps your models and controllers clean
Bonus: If your query returns an ActiveRecord::Relation
, you can still chain it like:
ActiveUsersQuery.new(7).call.order(:created_at).limit(10)
📦 In short, Query Objects help you write better, cleaner, and more powerful queries — without cluttering your models or controllers.
📘 Key Terms & Concepts – Query Objects in Rails
🔖 Term / Concept | 📄 Description |
---|---|
Query Object | A plain Ruby class that wraps a complex or reusable ActiveRecord query into a single object. |
call method | The standard public method used in Query Objects to execute and return the query result. |
app/queries/ | Recommended folder to store all query object files in your Rails project. |
ActiveRecord::Relation | The type of object returned by most Query Objects, which supports chaining (e.g., .order , .limit ). |
Chaining | The ability to call additional query methods after the Query Object returns a relation. |
Single Responsibility Principle (SRP) | Each Query Object should be responsible for just one specific query logic. |
Encapsulation | Hides the complex query logic behind a clean, easy-to-call interface. |
Testability | Query Objects are easy to write tests for because they are isolated from views and controllers. |
Named Scope | A simpler model-based alternative to Query Objects; useful for small, reusable queries. |
Class Method Queries | Commonly used in models but can lead to fat models; Query Objects are a better alternative for large logic. |
Query Caching | Query Objects can be combined with Rails caching mechanisms to avoid repeated DB hits. |
Reusability | Query Objects are designed to be reused in multiple places — services, controllers, jobs, etc. |
Parameterization | Query Objects can accept parameters like date ranges or user roles for flexible filtering. |
Return Type Consistency | Always return a consistent object type — usually ActiveRecord::Relation or an array of records. |
🔁 How Query Objects Work (Flow)
- 🧠 You identify a complex or repeating query
Example: active users in the last 7 days, top orders, inactive customers. - 📦 You create a Query Object class
Located inapp/queries
and named likeActiveUsersQuery
. - 🛠️ Inside the class, you define a
call
method
It contains the query logic and returns the result. - 📲 You use it wherever needed
Controller, service object, job, API response — just call:ActiveUsersQuery.new.call
- ✅ Query Object returns a relation or result
You get filtered, scoped, testable results — clean and easy.
Summary:
Feature Need → Create Query Object → Write call
→ Use Anywhere → Maintain Easily
📌 Where & How to Use Query Objects
Use Query Objects when your query is:
- 🔁 Repeated in multiple places
- 📏 Getting too long or complex
- 🧪 Needs to be tested separately
- 🎯 Has dynamic parameters (e.g., date ranges, roles)
Common Areas to Use Query Objects in Rails:
📍 Use Case | 💡 Description |
---|---|
👤 Active Users Report | Filter users who logged in during the last N days. |
📦 Orders with Pending Shipments | Find orders where shipping has not started. |
💰 Top Selling Products | Query for products with the most sales in the last month. |
🛠 Used in Admin Dashboards | Quickly render charts and tables using reusable queries. |
🔍 Advanced Filters (Search) | User filters like category, price range, availability. |
📊 Analytics and Metrics | Daily/weekly reporting for active usage, purchases, etc. |
📬 API Endpoints | Return consistent query results in APIs without bloating the controller. |
🧠 Rule: If your query has logic + filters + repetition → move it to a Query Object.
🧩 Gems & Libraries for Query Objects in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
ActiveRecord (built-in) | Rails’ default ORM used in most Query Objects to return chained relations. | Base for all query logic — no extra gem needed. |
SearchObject | DSL-based gem for building simple and composable query objects. | Simple filtering, parameterized searches, API filtering logic. |
Ransack | Advanced search gem that builds queries from params using a clean DSL. | Admin panels, search filters, or public-facing query interfaces. |
Dry-Struct | Strict data structures that can wrap query filters or parameters safely. | Query object validation, search input contracts, parameter objects. |
PgSearch | Adds powerful full-text PostgreSQL search scopes to your models. | Full-text search queries wrapped inside a Query Object. |
Squeel (deprecated) | Extended query syntax for building cleaner ActiveRecord queries. | Only for legacy projects — use Arel or scopes instead. |
ActiveFilter | Minimal gem for chaining and composing query filters using scopes. | API filters or composable admin query layers. |
ScopesFor | Generates query scopes dynamically based on params or filters. | Form-based queries or user-defined filters. |
🛠️ Best Implementation – Query Object Step-by-Step
This example demonstrates how to implement a Query Object to find users who logged in during the last N days. The goal is to keep this logic reusable, testable, and out of the controller or model.
📁 Step 1: Create a Folder for Query Objects
If it doesn’t exist already, create:
mkdir app/queries
📦 Step 2: Create a Query Object Class
File: app/queries/active_users_query.rb
class ActiveUsersQuery
def initialize(days = 30)
@days = days
end
def call
User.where(active: true)
.where("last_login_at >= ?", @days.days.ago)
.order(last_login_at: :desc)
end
end
- ✅ Uses plain Ruby class
- 🧪 Encapsulates logic inside a clean
call
method - 📦 Accepts parameters like
days
for flexibility - 🔁 Returns a relation for chaining and efficient queries
🎮 Step 3: Use in a Controller
File: app/controllers/admin/users_controller.rb
class Admin::UsersController < ApplicationController
def index
@active_users = ActiveUsersQuery.new(14).call
end
end
✅ Benefit: Your controller stays short and readable.
🧪 Step 4: Write a Test for the Query Object
File: spec/queries/active_users_query_spec.rb
require "rails_helper"
RSpec.describe ActiveUsersQuery do
it "returns users logged in within given days" do
old_user = create(:user, active: true, last_login_at: 60.days.ago)
recent_user = create(:user, active: true, last_login_at: 2.days.ago)
create(:user, active: false, last_login_at: 1.day.ago)
result = described_class.new(7).call
expect(result).to include(recent_user)
expect(result).not_to include(old_user)
expect(result).to all(be_active)
end
end
🧪 Benefit: Queries are now testable in isolation — no need to spin up the full controller.
📝 Summary – Key Concepts Used
- ✅ Plain Ruby class, no external dependencies
- 📦 Located in
app/queries/
- 🧼 One clear responsibility: a specific reusable query
- 🧪 Testable like any service or model
- ♻️ Reusable across controllers, jobs, mailers, and services
Pro Tip: Keep all your query objects consistent — name them with Query
suffix and expose only one method: call
.
💡 Example
File: app/queries/active_users_query.rb
class ActiveUsersQuery
def initialize(recent_days = 30)
@recent_days = recent_days
end
def call
User.where(active: true)
.where("last_login_at >= ?", @recent_days.days.ago)
end
end
Usage in controller or service:
active_users = ActiveUsersQuery.new(14).call
🔁 Alternative Concepts
- Named scopes in the model (less flexible)
- Service Objects with query logic inside (not ideal separation)
- Class methods inside models (tighter coupling)
🛠️ Technical Q&A – Query Objects in Rails
Q1: What is a Query Object in Rails?
A: A Query Object is a plain Ruby class used to extract and organize complex or reusable database queries outside of the model or controller.
# app/queries/active_users_query.rb
class ActiveUsersQuery
def call
User.where(active: true)
end
end
Q2: What method do you use to trigger a Query Object?
A: Conventionally, Query Objects expose a single public method called call
.
ActiveUsersQuery.new.call
Q3: Why use Query Objects instead of model scopes?
A: Use Query Objects when queries become too complex, have multiple conditions, or need parameters — keeping your model clean and adhering to SRP (Single Responsibility Principle).
# Too much in model
scope :active_recent, ->(days) {
where(active: true).where("last_login_at >= ?", days.days.ago)
}
# ✅ Cleaner in query object
class ActiveUsersQuery
def initialize(days = 30); @days = days; end
def call
User.where(active: true).where("last_login_at >= ?", @days.days.ago)
end
end
Q4: Where should you store Query Objects in Rails?
A: The recommended location is app/queries
.
Q5: What should a Query Object return?
A: Preferably an ActiveRecord::Relation
so you can chain it with pagination, ordering, etc.
TopOrdersQuery.new.call.limit(5)
Q6: Can you pass parameters to Query Objects?
A: Yes! Accept parameters through initialize
and use them in the call
method.
class OrdersByStatusQuery
def initialize(status)
@status = status
end
def call
Order.where(status: @status)
end
end
Q7: How do you test a Query Object?
A: Like any Ruby class — test the output of call
based on created data.
describe ActiveUsersQuery do
it "returns only active users" do
active = create(:user, active: true)
inactive = create(:user, active: false)
result = ActiveUsersQuery.new.call
expect(result).to include(active)
expect(result).not_to include(inactive)
end
end
Q8: Can Query Objects be composed (combined)?
A: Yes, if they return ActiveRecord::Relation
. You can pass one object’s result to another.
recent_orders = RecentOrdersQuery.new.call
shipped_orders = recent_orders.where(status: 'shipped')
Q9: Should Query Objects contain business logic?
A: No — they should only contain database access logic (queries). Business rules belong in service objects or models.
Q10: What are the benefits of Query Objects?
A:
- 🧼 Cleaner controller and model
- 🔁 Reusable across the app
- 🧪 Easier to test
- 💡 More readable and flexible query structure
✅ Best Practices with Examples
1. Use the Query
suffix in class names
This makes it clear the class is responsible for fetching data.
# ✅ Good
TopUsersQuery
ActiveOrdersQuery
2. Put all Query Objects in app/queries
Keep your folder structure organized.
app/
queries/
active_users_query.rb
recent_orders_query.rb
3. Only expose one public method: call
Keep a simple interface across all query objects.
def call
# return relation or data
end
4. Always return ActiveRecord::Relation
if possible
This allows further chaining, like pagination, sorting, etc.
def call
User.where(active: true).order(created_at: :desc)
end
5. Accept dynamic parameters via initialize
Make your query objects flexible and reusable.
class OrdersByStatusQuery
def initialize(status)
@status = status
end
def call
Order.where(status: @status)
end
end
6. Keep business logic out of Query Objects
Query objects should focus only on data retrieval — not side effects.
# ❌ Bad
def call
orders = Order.where(status: 'paid')
EmailService.send_alert(orders) # 👎
end
7. Write unit tests for each query object
Query Objects are easy to test because they are isolated and pure.
describe RecentPostsQuery do
it "returns posts from last 7 days" do
post = create(:post, created_at: 3.days.ago)
old_post = create(:post, created_at: 30.days.ago)
result = described_class.new(7).call
expect(result).to include(post)
expect(result).not_to include(old_post)
end
end
8. Chainable Query Objects are best
If you return a relation, you can chain, paginate, and reuse more easily.
TopUsersQuery.new.call.limit(10).page(1)
9. Use descriptive method names if returning multiple queries
But default to call
if you only need one interface.
def by_country; ... end
def by_status; ... end
10. Compose multiple Query Objects if needed
You can pass one query result into another to keep things modular.
verified = VerifiedUsersQuery.new.call
admin_verified = verified.where(role: 'admin')
🌍 Real-World Use Cases
- 🧍♂️ Fetching active or premium users
- 🛍 Getting top-selling products in the last 30 days
- 📦 Orders with delayed shipments
- 🔎 Custom admin dashboard filters
- 📊 Reusable analytics filters for reports
Decorator Pattern in Rails
🧠 Detailed Explanation
The Decorator Pattern is a way to add extra features or formatting to an object — without changing the object itself.
In Rails, we use decorators to format data for the view without cluttering the model or the controller.
Imagine you have a User
model with first_name
and last_name
. You want to show the full name like “Ali Khan” in the view — but you don’t want to add a method like full_name
inside the model. That’s where a decorator helps!
It wraps your model and adds view-specific logic like formatting names, dates, prices, etc.
🔧 What does it look like?
# user_decorator.rb
class UserDecorator
def initialize(user)
@user = user
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
def joined_on
@user.created_at.strftime("%B %d, %Y")
end
end
🧠 How is it used?
@decorated_user = UserDecorator.new(@user)
@decorated_user.full_name # "Ali Khan"
@decorated_user.joined_on # "May 24, 2025"
🎯 Why is it useful?
- ✅ Keeps your models clean — no view logic inside
- ✅ Keeps your views readable — no long formatting code
- ✅ Follows Single Responsibility Principle — formatting belongs in decorators
- ✅ Easy to test — decorators are plain Ruby classes
🔁 Think of it like:
- 📦 A “wrapper” that gives the same object extra powers
- 🎨 A presentation layer that’s separate from your business logic
- 📺 A TV remote that doesn’t change the TV — it just gives you a better way to interact with it
Decorator Pattern = Your model + display logic, cleanly separated.
📘 Key Terms & Concepts – Decorator Pattern in Rails
🔖 Term / Concept | 📄 Description |
---|---|
Decorator Pattern | A design pattern that wraps an object to add presentation logic without altering the original object’s behavior. |
UserDecorator (example) | A class that wraps a User model and adds formatted methods like full_name or joined_on . |
app/decorators | The recommended folder to store decorator classes in a Rails application. |
SimpleDelegator | A Ruby standard library class that forwards unknown methods to the wrapped object. Used to avoid manually delegating methods. |
Formatting Logic | Any code that formats dates, names, numbers, or other values for display. This logic belongs in decorators, not models or views. |
View Object | A broader term that includes decorators and presenters — used to manage how data is shown in the view. |
Single Responsibility Principle | A principle that states each class should have one purpose — decorators help keep models focused on data, not display logic. |
Draper Gem | A popular Rails gem that provides a framework for building decorators, with helpers and Rails integration out of the box. |
Presenter | A similar pattern to decorators, often used when you need to combine multiple models or view objects together. |
Delegation | The process of forwarding method calls to the original object, so the decorator behaves like the original model plus enhancements. |
Composition over Inheritance | Decorator pattern uses composition (wrapping the object) rather than inheritance (extending it), offering more flexibility. |
Encapsulation | The decorator encapsulates formatting logic separately from models, reducing coupling and improving testability. |
🔁 How the Decorator Pattern Works
- 🧱 You have a model
Example:User
model withfirst_name
andlast_name
. - 🎨 You want to display it differently in the view
Like showingfull_name
or a formatted date. - 📦 You create a decorator class
This class wraps the model and adds custom formatting or display methods. - 🔁 You use the decorator in the controller or view
Instead of calling@user.name
, you call@decorated_user.full_name
. - ✅ The view stays clean, the model stays clean, and your logic is testable
Model ➝ Decorator ➝ View
(User) (UserDecorator) (user.full_name)
📌 Where & How to Use Decorators in Rails
Decorators are best used when you want to format or prepare data for display, without bloating your model or view files.
📍 Use Case | 💡 Description |
---|---|
👤 User Profile Display | Format user names (full_name ), dates (joined_on ), or roles. |
🧾 Invoices | Format total amounts, currencies, and tax labels separately from the billing logic. |
📅 Event Pages | Convert raw date/time fields into human-friendly formats like “Tomorrow at 5PM”. |
🛒 Product Listings | Decorate a product model with custom price display, labels, or stock messages. |
📦 Admin Dashboards | Use decorators to simplify logic shown in admin views without polluting the model. |
💬 Notification System | Format user notifications like “Ali commented on your post 3 hours ago”. |
🧠 Rule: If it’s only needed for views and formatting, use a decorator — not a model or helper.
🧩 Gems & Libraries for the Decorator Pattern
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
Draper | Most popular decorator gem for Rails. Adds helpers, view context, and Rails integration to decorators. | Decorating models for views with access to helpers and view logic. |
SimpleDelegator (Ruby stdlib) | A base class from the delegate module that forwards unknown methods to the wrapped object. | Lightweight custom decorators without external gems. |
Forwardable (Ruby stdlib) | Provides selective method delegation using def_delegators for clean Ruby objects. | Custom delegation for performance or control over exposed methods. |
ViewComponent | Not a decorator gem, but encourages clean, testable components that serve a similar purpose. | When you need rich UI logic beyond just formatting values. |
Presenter (custom) | A variation of the decorator pattern, used to combine multiple objects into a single view model. | When decorating multiple models or formatting collections of data. |
ActiveDecorator (less common) | Automatically decorates your models when passed to views. Lightweight alternative to Draper. | Auto-decorating models globally without manual wrapping. |
🛠️ Best Implementation – Using SimpleDelegator
(Native Ruby)
Goal: Display a user’s full name and nicely formatted join date without polluting the model or the view.
📁 Step 1: Create the Decorator File
File: app/decorators/user_decorator.rb
require 'delegate'
class UserDecorator < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
def joined_date
created_at.strftime("%B %d, %Y")
end
end
- ✅
SimpleDelegator
auto-forwards all methods to the original object - 🧼 Only enhanced presentation logic lives here
🎮 Step 2: Wrap the Model in the Controller
# app/controllers/users_controller.rb
def show
@user = User.find(params[:id])
@user = UserDecorator.new(@user)
end
✅ Now @user.full_name
and @user.joined_date
are available in the view.
🖼️ Step 3: Use in the View
<p>Welcome, <%= @user.full_name %></p>
<p>Joined on: <%= @user.joined_date %></p>
🧪 Step 4: Test the Decorator
# spec/decorators/user_decorator_spec.rb
describe UserDecorator do
it "returns full name" do
user = User.new(first_name: "Ali", last_name: "Khan", created_at: Time.new(2024, 5, 1))
decorated = UserDecorator.new(user)
expect(decorated.full_name).to eq("Ali Khan")
expect(decorated.joined_date).to eq("May 01, 2024")
end
end
✅ Easy to test, no database needed!
🛠️ Best Implementation – Using Draper
Gem
Use Draper when you need decorators with access to Rails view helpers (like number_to_currency
, link_to
, etc.).
🔧 Step 1: Install Draper
bundle add draper
rails generate decorator User
📁 Step 2: Add Display Logic
# app/decorators/user_decorator.rb
class UserDecorator < Draper::Decorator
delegate_all
def full_name
"#{object.first_name} #{object.last_name}"
end
def joined_date
h.time_ago_in_words(object.created_at) + " ago"
end
end
h
gives you access to Rails view helpers inside the decorator.
🎮 Step 3: Decorate the Model
# users_controller.rb
def show
@user = User.find(params[:id]).decorate
end
🖼️ Step 4: Use in the View
<p><%= @user.full_name %></p>
<p>Joined: <%= @user.joined_date %></p>
💎 Optional: Automatically Decorate in Views
# config/application.rb
config.to_prepare do
ApplicationController.helper Draper::HelperSupport
end
This allows you to call @user.full_name
in views even without decorating in the controller.
💡 Example
File: app/decorators/user_decorator.rb
class UserDecorator
def initialize(user)
@user = user
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
def formatted_join_date
@user.created_at.strftime("%B %d, %Y")
end
end
Usage in view/controller:
@decorated_user = UserDecorator.new(@user)
@decorated_user.full_name
🔁 Alternatives
- Helpers (good for simple formatting, not object-oriented)
- View Models (broader presentation objects)
- Presenter Pattern (similar but usually combines multiple models)
🛠️ Technical Q&A with Examples – Decorator Pattern in Rails
Q1: What is the purpose of a decorator in Rails?
A: To wrap a model and add presentation logic (like formatting or computed values) without modifying the model itself.
Q2: How do you create a decorator using Ruby only?
A: Use Ruby’s SimpleDelegator
to forward all method calls to the original object.
class UserDecorator < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
end
decorated = UserDecorator.new(User.find(1))
decorated.full_name
Q3: What’s the difference between a decorator and a presenter?
A: A decorator
formats one object. A presenter
often combines multiple objects or prepares data for a full view.
Q4: How can a decorator access Rails view helpers?
A: Use the Draper
gem. It provides a helper proxy via h
.
def formatted_price
h.number_to_currency(object.total)
end
Q5: How do you test a decorator?
A: Like any Ruby object. Pass a model and assert decorated output.
it "formats full name" do
user = User.new(first_name: "Ali", last_name: "Khan")
decorator = UserDecorator.new(user)
expect(decorator.full_name).to eq("Ali Khan")
end
Q6: Where should decorators be stored in a Rails app?
A: Conventionally, in app/decorators/
.
Q7: What does delegate_all
do in Draper?
A: It tells the decorator to forward all methods to the original model unless explicitly overridden.
Q8: Can decorators be used in collections?
A: Yes. With Draper: @users = User.all.decorate
. For custom, use map { |u| UserDecorator.new(u) }
.
Q9: What type of logic should NOT go into decorators?
A: Business logic, database queries, or persistence — keep those in models or services.
Q10: When should you choose a decorator over a helper?
A: Use a decorator when formatting is tied to an object instance and benefits from method chaining and encapsulation.
✅ Best Practices with Examples – Decorator Pattern in Rails
1. Use decorators for view-specific logic only
Decorators should format data — not handle business rules or persistence.
# Good
def full_name
"#{first_name} #{last_name}"
end
# Bad
def save_user
user.save # ❌ belongs in model or service
end
2. Store decorators in app/decorators
Keep your app clean and easy to navigate.
# File path:
app/decorators/user_decorator.rb
3. Always name decorators with *Decorator
suffix
Clear and searchable naming makes code self-documenting.
# Good
UserDecorator, OrderDecorator
# Bad
UserViewHelper, FormattedUser
4. Delegate unknown methods to the wrapped model
Use SimpleDelegator
or delegate_all
(Draper) to make your decorator act like the model.
class ProductDecorator < SimpleDelegator
def formatted_price
"$#{'%.2f' % price}"
end
end
5. Don’t decorate in the view — decorate in the controller
Keep logic centralized and consistent.
# Good (controller)
@user = UserDecorator.new(User.find(params[:id]))
# Bad (view)
UserDecorator.new(@user).full_name
6. Use Draper when you need helpers like number_to_currency
Draper provides a clean interface to view helpers through the h
object.
def total_price
h.number_to_currency(order.total)
end
7. Avoid heavy logic or conditional trees
Keep decorators focused on formatting and readability.
# ✅ Keep it clean
def status_label
order.status.titleize
end
8. Use decorators for collections when needed
@decorated_users = @users.map { |u| UserDecorator.new(u) }
9. Test decorators in isolation
You don’t need the full Rails stack to test them.
it "returns full name" do
user = User.new(first_name: "Ali", last_name: "Khan")
decorated = UserDecorator.new(user)
expect(decorated.full_name).to eq("Ali Khan")
end
10. Chain decorators or combine them in presenters when necessary
For multi-model views, consider using presenters or view objects composed of multiple decorators.
🌍 Real-World Use Cases
- 🧾 Formatting invoice totals with currency and tax
- 👤 Displaying user profile info like “Full Name” or “Joined on…”
- 📅 Formatting date ranges for events
- 📦 Showing shipping status based on delivery logic
- 💬 Showing time ago like “3 hours ago” using
time_ago_in_words
Policy Pattern in Rails (Authorization)
🧠 Detailed Explanation
The Policy Pattern is used in Rails to control what actions a user is allowed to perform on a resource (like a post, comment, or order).
It’s part of the authorization system, which answers the question:
“Is this user allowed to do this?”
Instead of writing permission logic in your controllers or models, the policy pattern puts that logic in a separate file called a Policy.
Each model (like Post
) can have a matching PostPolicy
class. That class has methods like:
show?
update?
destroy?
These methods return true
or false
depending on whether the user can do the action.
🔧 Example
# app/policies/post_policy.rb
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? || user == post.author
end
def destroy?
user.admin?
end
end
📦 How it works
- You create a policy class (e.g.,
PostPolicy
) - Inside that class, you define methods like
update?
- In your controller, you write
authorize @post
- Rails checks if
PostPolicy#update?
returnstrue
# Controller
def update
@post = Post.find(params[:id])
authorize @post # checks if current_user can update it
@post.update(post_params)
end
✅ Why it’s useful
- 💡 Keeps authorization logic out of controllers
- 🧪 Easy to test (each policy is a plain Ruby class)
- 🔁 Reusable — use the same logic across controllers and views
- 📦 Clean and consistent structure for all models
Summary: The Policy Pattern lets you keep all your “who can do what” logic in one clean place — separate from models and controllers.
📘 Key Terms & Concepts – Policy Pattern (Authorization) in Rails
🔖 Term / Concept | 📄 Description |
---|---|
Policy Pattern | A design pattern that separates authorization logic into dedicated policy classes that determine whether a user can perform an action. |
Policy Class | A plain Ruby class (e.g., PostPolicy ) containing methods like update? , destroy? that return true/false for user permissions. |
authorize | A method used in the controller to check if a user is allowed to perform an action based on the related policy. |
Pundit | A popular Rails gem that implements the Policy Pattern with helpers like authorize , policy_scope , and automatic inference of policy classes. |
Policy Scope | A nested class (e.g., PostPolicy::Scope ) that defines which records a user is allowed to see (commonly used in index actions). |
Action Method (e.g., update? ) | Methods defined in the policy class to represent permissions for specific controller actions. |
app/policies | The conventional directory in a Rails app where all policy classes are stored. |
User Context | The current user instance passed into the policy class (e.g., current_user ) used to make authorization decisions. |
Record | The object being authorized (e.g., a specific Post ), passed to the policy to check access rules. |
Fallback Policy | A default policy used when no specific one is defined. Often used to prevent unauthorized access globally. |
before_action :authorize | A common controller hook that triggers policy checks before executing an action. |
Permission Denied | An error (e.g., Pundit::NotAuthorizedError ) raised when a user fails an authorization check. |
Testability | Policy classes are pure Ruby objects, which makes them easy to unit test outside the controller. |
🔁 Flow – How the Policy Pattern Works
- 🧱 A user triggers a controller action
Example: Clicking “Edit Post” sends a request to theupdate
action. - 🔐 The controller calls
authorize
This checks if the current user is allowed to perform that action using the corresponding policy. - 📄 The policy class is loaded
Rails looks for a matching policy class (e.g.,PostPolicy
). - ✅ The relevant method is called
Forauthorize @post
insideupdate
, Rails callsPostPolicy#update?
. - 🔁 The method returns true or false
Based on the logic inside the policy, the action is either allowed or denied. - 🚫 If false, Rails raises an authorization error
You can handle this with a rescue block or error page.
# Controller
def update
@post = Post.find(params[:id])
authorize @post # calls PostPolicy#update?
@post.update(post_params)
end
📌 Where & How to Use Policy Pattern
Use the Policy Pattern whenever you want to control who can do what in your app. It separates authorization rules from your controllers and models.
📍 Use Case | 💡 Description |
---|---|
✏️ Update / Edit Resources | Check if a user can edit a post, comment, order, or record based on ownership or role. |
🗑️ Deleting Items | Allow only certain users (like admins) to delete records. |
👀 Access Control (Show / Index) | Limit visibility of data — e.g., users can only view their own orders or profiles. |
⚙️ Admin-Only Actions | Restrict access to admin dashboards, settings, and management tools. |
📤 Download Permissions | Only allow authorized users to download reports, invoices, or sensitive files. |
🧾 Invoicing and Billing | Ensure that finance-related data is only accessible to the finance team or relevant users. |
💬 Comments or Feedback | Only let authors edit or delete their own comments — not others’. |
💡 Pro Tip: Use the Policy Pattern anytime your logic includes “only if user is X” or “unless admin” conditions.
🧩 Gems & Libraries for Policy Pattern in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
Pundit | A clean and minimal authorization gem for Rails based on the Policy Pattern. Adds helpers like authorize and policy_scope . | Most popular gem for defining user-specific access rules per model and action. |
Action Policy | A modern alternative to Pundit, with support for nested rules, caching, and GraphQL compatibility. | High-performance apps, GraphQL APIs, and modular policies. |
CanCanCan | Older and widely used authorization gem using an Ability class to define user permissions. | Still useful for legacy apps or centralized rule definitions. |
rolify | Role management gem — allows assigning roles like admin, editor, manager to users. | Works well alongside Pundit or CanCanCan to assign permissions by role. |
devise + pundit | Combo commonly used in Rails: Devise handles authentication, Pundit handles authorization. | Best-practice stack for secure Rails applications. |
AccessGranted | A lightweight and flexible authorization gem based on roles and policies. | Smaller apps where role-based permissions are enough. |
Declarative Authorization | A DSL-based gem for defining permissions in YAML and Ruby. Now mostly legacy. | Legacy systems still using YAML-based access control. |
🛠️ Best Implementation – Policy Pattern with Pundit (Rails)
This guide shows how to implement the Policy Pattern for a Post
model, allowing only admins or post authors to update or delete a post.
🔧 Step 1: Install Pundit
bundle add pundit
Then run:
rails generate pundit:install
This adds ApplicationPolicy
and includes Pundit in your ApplicationController
.
📄 Step 2: Create a Policy for the Model
File: app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? || user == post.author
end
def destroy?
user.admin?
end
def show?
true # public post
end
end
✅ Note: Each method ends with ?
and returns true
or false
.
🎮 Step 3: Use authorize
in the Controller
File: app/controllers/posts_controller.rb
class PostsController < ApplicationController
include Pundit
def update
@post = Post.find(params[:id])
authorize @post # Calls PostPolicy#update?
@post.update(post_params)
redirect_to @post, notice: "Updated!"
end
def destroy
@post = Post.find(params[:id])
authorize @post # Calls PostPolicy#destroy?
@post.destroy
redirect_to posts_path, notice: "Deleted!"
end
end
✅ Pro Tip: Use authorize
in every action that modifies data.
🧪 Step 4: Test the Policy Class
File: spec/policies/post_policy_spec.rb
(RSpec)
RSpec.describe PostPolicy do
let(:admin) { User.new(role: "admin") }
let(:author) { User.new(id: 1) }
let(:other_user) { User.new(id: 2) }
let(:post) { Post.new(author: author) }
subject { described_class }
it "allows admin to update" do
expect(subject.new(admin, post).update?).to be true
end
it "allows author to update" do
expect(subject.new(author, post).update?).to be true
end
it "denies other user" do
expect(subject.new(other_user, post).update?).to be false
end
end
🛡️ Step 5: Handle Unauthorized Access Gracefully
# application_controller.rb
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "Access denied!"
redirect_to(request.referer || root_path)
end
✅ This shows a friendly message when access is denied.
📊 Bonus: Use Policy Scopes for Index Actions
File: post_policy.rb
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.where(published: true)
end
end
end
In controller:
@posts = policy_scope(Post)
✅ Now users only see what they’re allowed to see.
💡 Example
Policy File: app/policies/post_policy.rb
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? || user == post.author
end
def destroy?
user.admin?
end
end
Usage in controller:
def update
@post = Post.find(params[:id])
authorize @post # Will call PostPolicy#update?
@post.update(post_params)
end
🔁 Alternative Methods
- CanCanCan: Uses ability files but tightly couples authorization logic to models.
- Rolify: Adds roles, but doesn’t define logic per action.
- before_action: Works, but leads to scattered logic and duplication.
🛠️ Technical Questions & Answers – Policy Pattern (Rails + Pundit)
Q1: What is a policy in Rails?
A: A policy is a class that defines user permissions for actions on a model (e.g., update?
, destroy?
).
class PostPolicy
def update?
user.admin? || user == post.author
end
end
Q2: How does authorize
work?
A: It automatically finds the related policy (e.g., PostPolicy
) and checks the action (e.g., update?
).
# In controller
authorize @post # Calls PostPolicy#update?
Q3: Where should policies be placed in a Rails app?
A: In app/policies/
. Rails will auto-load them if the naming matches.
Q4: What is policy_scope
used for?
A: It’s used for index actions to filter which records a user can access.
# In controller
@posts = policy_scope(Post)
# In policy
class PostPolicy::Scope < Scope
def resolve
user.admin? ? scope.all : scope.where(published: true)
end
end
Q5: What does Pundit::NotAuthorizedError
mean?
A: It means the user failed the policy check. You should rescue it.
rescue_from Pundit::NotAuthorizedError do
flash[:alert] = "Access denied"
redirect_to root_path
end
Q6: Can you pass extra arguments into policies?
A: Yes, but it’s not built-in. Use custom methods or service objects instead of overloading the constructor.
Q7: How do you test a policy?
A: Test each permission method by creating user and resource instances and calling the policy method directly.
it "allows admin to destroy" do
admin = User.new(role: "admin")
post = Post.new
expect(PostPolicy.new(admin, post).destroy?).to be true
end
Q8: What does delegate
mean in policies?
A: It’s used to forward calls to the user or model object to reduce repetition, e.g., delegate :admin?, to: :user
.
Q9: Should you use one policy per model?
A: Yes. Each model should have its own policy class. This keeps permission logic isolated and clean.
Q10: What’s the difference between authorize
and policy_scope
?
A:
authorize
is for single records (e.g., show, update, delete)policy_scope
is for lists (e.g., index views)
✅ Best Practices with Examples – Policy Pattern in Rails
1. Use One Policy Per Model
Each model (e.g., Post
) should have its own policy (e.g., PostPolicy
).
# app/policies/post_policy.rb
class PostPolicy
def update?
user.admin? || user == post.author
end
end
2. Name Methods After Actions
Use the same names as your controller actions: show?
, create?
, update?
, destroy?
.
def destroy?
user.admin?
end
3. Always Use authorize
in Controllers
This enforces security and prevents accidental exposure.
# PostsController
def update
@post = Post.find(params[:id])
authorize @post
@post.update(post_params)
end
4. Use policy_scope
in Index Actions
Limit data visibility to only what the user is allowed to see.
@posts = policy_scope(Post)
class PostPolicy::Scope < Scope
def resolve
user.admin? ? scope.all : scope.where(published: true)
end
end
5. Avoid Complex Conditionals
Split conditions into helper methods or use role-based delegation for clarity.
def update?
admin_or_owner?
end
private
def admin_or_owner?
user.admin? || user == post.author
end
6. Handle Unauthorized Access Globally
Rescue Pundit::NotAuthorizedError
in your ApplicationController
.
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized
flash[:alert] = "Access denied."
redirect_to(request.referer || root_path)
end
7. Test Your Policies
Use RSpec or Minitest to test each policy method with different user roles.
it "allows admin to destroy posts" do
user = User.new(role: "admin")
post = Post.new
expect(PostPolicy.new(user, post).destroy?).to be true
end
8. Use ApplicationPolicy for Defaults
Define common methods like admin?
once and inherit them in other policies.
# app/policies/application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def admin?
user.admin?
end
end
9. Authorize Nested Resources with Care
When dealing with nested routes, always authorize the right object (e.g., @comment
not @post
).
10. Don’t Authorize in Views
Use policy(@post).update?
sparingly in views. Prefer to prepare authorization status in the controller.
🌍 Real-World Scenarios
- 🛡 A user can only edit their own blog posts.
- 🔒 Only admins can delete records.
- 👀 Users can only see projects they belong to.
- 💬 Comments can be edited by their authors but not others.
- 🧾 Invoices can only be viewed by the finance team or the customer who owns it.
Presenter Pattern in Rails
🧠 Detailed Explanation
The Presenter Pattern is used in Rails to organize and simplify view-related logic that doesn’t belong in models or controllers.
Think of it like a **helper object** that “presents” your data the way your view needs it — combining, formatting, or enriching data from one or more models.
🎯 Why use it?
- ✅ Keep models focused on data and business logic
- ✅ Keep controllers clean from formatting code
- ✅ Make your views easier to read and maintain
- ✅ Combine multiple models if needed (e.g., User + Order)
📦 Example Use Case
You want to show a user’s full name, their join date (in words), and status badge in the UI.
Instead of cluttering the model or the view with that logic, you put it in a presenter:
# app/presenters/user_presenter.rb
class UserPresenter
def initialize(user, view)
@user = user
@view = view
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
def joined_date
@view.time_ago_in_words(@user.created_at) + " ago"
end
def status_label
@user.active? ? "✅ Active" : "⛔ Inactive"
end
end
And then use it in your view like this:
@user_presenter = UserPresenter.new(@user, view_context)
<p>Name: <%= @user_presenter.full_name %></p>
<p>Joined: <%= @user_presenter.joined_date %></p>
<p>Status: <%= @user_presenter.status_label %></p>
🧠 Think of it like:
- 🎨 A paintbrush that makes your raw data ready for display
- 📦 A wrapper that keeps logic out of your model and view
- 🔌 A clean plug between controller and view
Summary: The Presenter Pattern is all about making views smarter without making your models and controllers messy.
📘 Key Terms & Concepts – Presenter Pattern in Rails
🔖 Term | 📄 Description |
---|---|
Presenter Pattern | A design pattern that extracts view logic from controllers and models into a separate class called a presenter. |
Presenter | A Ruby object that formats data for views, often combining multiple models or adding helper methods like full name, status badges, etc. |
view_context | A Rails object passed into the presenter so it can use helpers like link_to , time_ago_in_words , etc. |
full_name | A common example of a method you’d put in a presenter, formatting two fields (first and last name) into one for display. |
status_label | A custom formatting method used in presenters to generate display-friendly statuses (e.g., Active, Inactive). |
app/presenters | The recommended folder in Rails apps where presenter classes are stored for organization. |
Decorators vs Presenters | Decorators wrap one model and format data. Presenters can combine multiple models and view helpers. |
Service Object | Another pattern used alongside presenters — presenters handle display, services handle business actions. |
Composition | Presenters often use composition, meaning they use one or more objects (e.g., @user , @order ) rather than inheriting from them. |
Single Responsibility Principle | The presenter follows this principle by separating presentation logic from business logic and data access. |
DRY Views | One of the key reasons to use presenters is to keep your views clean and DRY (Don’t Repeat Yourself). |
🔁 Flow – How the Presenter Pattern Works
- 💾 A model is fetched in the controller
Example:@user = User.find(params[:id])
- 🎁 A presenter is initialized with the model
Example:@user_presenter = UserPresenter.new(@user, view_context)
- 🧠 The presenter formats or combines data
The presenter adds methods likefull_name
,joined_on
,status_badge
- 🖼 The view displays presenter methods
Example:<%= @user_presenter.full_name %>
Controller ➝ Presenter ➝ View
(model) (format logic) (output)
✅ This flow keeps formatting logic out of both the model and view.
📌 Where & How to Use Presenter Pattern
Use presenters anywhere you need to format, combine, or cleanly display model data in your views — especially when it starts cluttering your ERB or model files.
📍 Use Case | 💡 Description |
---|---|
👤 User Dashboards | Combine account info, profile details, and formatted metadata into one view object. |
🛍 Order Summary Pages | Display customer, products, prices, tax totals, and status badges without bloating the model or view. |
📊 Admin Panels | Present a collection of models (users, payments, stats) with clean HTML-ready formatting. |
📬 Email Templates | Format and structure complex message content from multiple models for display in views or mailers. |
📆 Calendars or Reports | Build timelines, lists, and overviews from a mix of data like schedules, appointments, or activity logs. |
📦 Combined Objects | Use when your view needs logic that combines more than one model (e.g., User + Invoice ). |
💡 Rule of Thumb: Use presenters when your view includes more than 2–3 conditionals, or when your model starts mixing display logic.
🧩 Gems & Libraries for Presenter Pattern in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
No gem needed | The Presenter Pattern is often implemented using plain Ruby objects — no external gem required. | Best for clean, minimal, and custom presenter logic. |
Draper | A popular gem designed for decorators, but often used as presenters too. Supports view context, helpers, and automatic wrapping. | Great for single-object presentation logic or projects already using Draper. |
ActivePresenter (legacy) | An old gem used to manage form objects and multi-model views. Now mostly deprecated or replaced by other patterns. | Use plain Ruby classes instead for modern Rails apps. |
ViewComponent (from GitHub) | Encapsulates both data and view in a reusable component. It’s more structured than presenters but solves a similar need. | When you want reusable, testable UI components with logic + markup together. |
Cells (Trailblazer) | Component-based view objects. More powerful than presenters, but more complex. Separates rendering and logic completely. | For large apps that need reusable, fully-isolated view units. |
SimpleDelegator | Ruby’s built-in class that can forward unknown method calls to a wrapped object — useful when presenter wraps a model directly. | Use for light delegation from presenter to model (e.g., UserPresenter < SimpleDelegator ). |
🛠️ Best Implementation – Pure Ruby Presenter in Rails
In this example, we’ll build a presenter to format and display user information like full name, join date, and status badge.
📁 Step 1: Create a Presenter Class
Path: app/presenters/user_presenter.rb
class UserPresenter
def initialize(user, view_context)
@user = user
@view = view_context
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
def joined_date
@view.time_ago_in_words(@user.created_at) + " ago"
end
def status_badge
if @user.active?
@view.content_tag(:span, "Active", class: "badge badge-success")
else
@view.content_tag(:span, "Inactive", class: "badge badge-secondary")
end
end
end
- ✅ Uses
@view
to access Rails helpers inside the presenter. - ✅ Only includes view logic — no business rules.
🎮 Step 2: Use Presenter in the Controller
File: users_controller.rb
def show
@user = User.find(params[:id])
@user_presenter = UserPresenter.new(@user, view_context)
end
🖼 Step 3: Call Presenter Methods in the View
File: views/users/show.html.erb
<h2><%= @user_presenter.full_name %></h2>
<p>Joined: <%= @user_presenter.joined_date %></p>
<p><%= @user_presenter.status_badge %></p>
✅ Your view is now clean, readable, and free from formatting logic.
🧪 Step 4: Test the Presenter
File: spec/presenters/user_presenter_spec.rb
require "rails_helper"
describe UserPresenter do
let(:user) { User.new(first_name: "Ali", last_name: "Khan", created_at: 2.days.ago, active: true) }
let(:view) { ActionView::Base.new }
let(:presenter) { UserPresenter.new(user, view) }
it "returns full name" do
expect(presenter.full_name).to eq("Ali Khan")
end
it "returns formatted join date" do
expect(presenter.joined_date).to include("ago")
end
it "returns status badge" do
expect(presenter.status_badge).to include("Active")
end
end
✅ Presenters are plain Ruby objects — easy to test!
📦 Bonus: Add Collection Presenter
To present lists of users cleanly:
class UserCollectionPresenter
def initialize(users, view)
@users = users
@view = view
end
def each
@users.map { |user| UserPresenter.new(user, @view) }
end
end
@user_list = UserCollectionPresenter.new(User.all, view_context)
<% @user_list.each.each do |presenter| %>
<%= presenter.full_name %>
<% end %>
💡 Example
Use case: Showing user full name, formatted date, and status in a dashboard.
# app/presenters/user_presenter.rb
class UserPresenter
def initialize(user, view)
@user = user
@view = view
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
def joined_on
@view.time_ago_in_words(@user.created_at) + " ago"
end
def status_tag
@user.active? ? @view.content_tag(:span, "Active", class: "badge badge-success") :
@view.content_tag(:span, "Inactive", class: "badge badge-secondary")
end
end
# In controller
@user_presenter = UserPresenter.new(@user, view_context)
# In view
<p><%= @user_presenter.full_name %></p>
<p>Joined: <%= @user_presenter.joined_on %></p>
<p><%= @user_presenter.status_tag %></p>
🔁 Alternative Methods
- Decorator Pattern: Similar, but wraps one model only.
- ViewComponent: Ideal for reusable visual components.
- Helpers: Fine for simple methods, not good for multiple model logic.
🛠️ Technical Questions & Answers – Presenter Pattern in Rails
Q1: What is the Presenter Pattern?
A: The Presenter Pattern is a design pattern that extracts view-specific logic from models and controllers into a separate object. This improves code readability, testability, and maintainability.
Q2: How is a presenter different from a helper?
A: Helpers are globally accessible and usually stateless. Presenters are **stateful** objects that are passed model(s) and view context, making them ideal for formatting and organizing complex view logic.
Q3: How do you create a presenter?
A: You create a plain Ruby class inside app/presenters/
and initialize it with a model and view_context
.
class UserPresenter
def initialize(user, view)
@user = user
@view = view
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
end
Q4: Why do we pass view_context
to presenters?
A: So the presenter can use Rails helpers like time_ago_in_words
, number_to_currency
, link_to
, etc.
@user_presenter = UserPresenter.new(@user, view_context)
Q5: Can a presenter access multiple models?
A: Yes. Presenters can be initialized with multiple models or fetch them internally. This makes them ideal for views that need combined data.
class OrderPresenter
def initialize(order, user, view)
@order = order
@user = user
@view = view
end
end
Q6: Where should presenters live in a Rails project?
A: In app/presenters
. You can auto-load this directory by adding it in config/application.rb
if needed.
Q7: How do you test a presenter?
A: Presenters are plain Ruby objects. You can test them with RSpec or Minitest like any other PORO (Plain Old Ruby Object).
describe UserPresenter do
it "returns full name" do
user = User.new(first_name: "Ali", last_name: "Khan")
view = ActionView::Base.new
presenter = UserPresenter.new(user, view)
expect(presenter.full_name).to eq("Ali Khan")
end
end
Q8: What problems does the presenter pattern solve?
A: It removes formatting and conditional logic from views and controllers, promotes reuse, makes views simpler, and keeps models focused on business logic.
Q9: What’s the difference between a presenter and a decorator?
A: A **decorator** wraps a single model and often adds behavior. A **presenter** can format one or more models and includes view helpers for presentation.
Q10: When should you not use a presenter?
A: When the logic is too simple (e.g., one-line method), a helper may suffice. Avoid presenters for business logic or service actions — they are only for presentation logic.
✅ Best Practices with Examples – Presenter Pattern in Rails
1. Name presenters after the resource
Use clear, singular names like UserPresenter
, OrderPresenter
, etc.
# Good
class UserPresenter
# Bad
class UserFormatHelper
2. Store presenters in app/presenters/
This keeps structure consistent and allows easy autoloading.
# structure
app/
├─ models/
├─ views/
├─ presenters/
│ └─ user_presenter.rb
3. Use view_context
to access Rails helpers
This allows presenters to use helpers like time_ago_in_words
or link_to
.
def joined_on
@view.time_ago_in_words(@user.created_at) + " ago"
end
4. Only include display logic — not business rules
Presenters should format data, not update or validate it.
# ✅ Good
def full_name
"#{@user.first_name} #{@user.last_name}"
end
# ❌ Bad
def activate_account
@user.update(active: true) # should be in model or service
5. Use presenters in controllers, not views
Set the presenter in the controller so views stay clean.
# Controller
@user_presenter = UserPresenter.new(@user, view_context)
# View
<%= @user_presenter.full_name %>
6. Keep presenters small and focused
Each presenter should do one job — don’t mix unrelated logic.
7. Combine multiple models only if needed
When you must present combined data (e.g., Order
+ User
), pass both into the presenter.
class OrderPresenter
def initialize(order, user, view)
@order = order
@user = user
@view = view
end
end
8. Write unit tests for presenters
Test each method independently, just like any other Ruby object.
it "returns full name" do
presenter = UserPresenter.new(user, view)
expect(presenter.full_name).to eq("Ali Khan")
end
9. Avoid database queries inside presenters
Fetch all required data in the controller and pass it to the presenter.
# ❌ Don't
@user.orders.last # querying inside presenter
# ✅ Do
@orders = @user.orders
@presenter = UserPresenter.new(@user, @orders, view_context)
10. Reuse presenters in mailers and APIs (view rendering)
Presenters are plain Ruby objects — perfect for reuse across views, PDFs, emails, etc.
🌍 Real-World Scenarios
- 🧑💼 User dashboards with account and profile info
- 🛒 Order summary cards that include user, items, and shipping status
- 📊 Admin panels combining reports, metrics, and user activity
- 🎯 Marketing views combining content and statistics
- 📦 Invoicing pages showing customer, payment, and line item data
ViewComponent Pattern in Rails
🧠 Detailed Explanation
The ViewComponent Pattern is a way to build small, reusable, and testable UI blocks in Rails using Ruby classes and templates.
Instead of writing lots of HTML in your views or repeating partials everywhere, you create a Component
— a Ruby class that has its own template file.
🔍 Simple Definition
ViewComponent = Ruby class + HTML template = reusable UI block.
It helps you:
- ✅ Organize your views
- ✅ Reuse the same UI logic across pages
- ✅ Write tests for each UI piece
📦 Example Use Case
Let’s say you want to show a colored alert box (success, error, info) across your app.
Instead of repeating HTML or logic everywhere, you can create:
- A Ruby class:
AlertComponent
- A template:
alert_component.html.erb
Then use it anywhere with:
<%= render(AlertComponent.new(message: "Success!", type: :success)) %>
Result:
[Success!] ✅ You’ve saved your changes.
🤔 Why Use It?
- 🎯 Keeps logic and markup together
- 💡 Makes your views cleaner
- 🧪 Easy to test in isolation
- 🔁 Works well with Tailwind, Bootstrap, or any CSS
🧠 Think of it like:
- 🔌 A Lego piece you can plug into different pages
- 🧩 A self-contained widget with logic + display
- 📦 A mini partial that can be tested and reused
In short: ViewComponents give you the power of React/Vue-style components inside Rails using familiar Ruby + ERB.
📘 Key Terms & Concepts – ViewComponent Pattern
🔖 Term | 📄 Description |
---|---|
ViewComponent | A Ruby class that renders an HTML template. It encapsulates both view logic and markup in a reusable, testable unit. |
Component Class | A Ruby class (e.g., AlertComponent ) that accepts arguments like message: or type: and prepares data for the template. |
Component Template | An ERB or HAML file (e.g., alert_component.html.erb ) that renders the actual HTML using instance variables set by the component class. |
render | The method used in views to call and render a component. Example: render(MyComponent.new(...)) . |
Slots | Named content areas inside components that allow you to pass blocks of HTML into reusable parts of a component (like headers, bodies). |
Previews | Files that render component examples visually in development, located in app/components/previews . Used to test and view components in isolation. |
render_inline | A test helper from ViewComponent’s testing tools used in RSpec to test components without rendering a full page. |
Encapsulation | The practice of keeping logic, data, and UI tightly packaged together in a component so it can work independently from the rest of the app. |
Reusability | One of the biggest advantages of components — once you build it, you can use it anywhere in your app without rewriting logic or HTML. |
Partial vs Component | Partials are templates only. Components include logic and structure, making them easier to test and reuse. |
🔁 Flow – How ViewComponent Works
- 1. You create a Component class
Example:app/components/button_component.rb
This file defines logic and accepts data (props). - 2. You create a template file
Example:app/components/button_component.html.erb
This file contains the actual HTML for the button. - 3. You render the component in a view
Example:<%= render(ButtonComponent.new(text: "Save", type: :primary)) %>
- 4. The component class processes the data
It sets variables like@text
and@type
for use in the template. - 5. The template renders HTML
Example:<button class="btn btn-{@type}">{@text}</button>
Result: Clean views, reusable logic, and testable HTML blocks.
📌 Where & How to Use ViewComponent in Rails
Use ViewComponents anywhere you repeat HTML + logic, or want reusable, testable UI.
📍 Use Case | 💡 Description |
---|---|
✅ Buttons & Tags | Style buttons or labels with dynamic text, color, and icon logic all in one place. |
📦 Product Cards | Display formatted data (name, price, image, tags) for each product using one component. |
📬 Alerts & Flash Messages | Reusable alert boxes with dynamic types (success, warning, error). |
📊 Admin Dashboards | Graphs, widgets, and metric cards can be turned into components for reuse. |
📅 Calendars & Schedules | Create each event as a component, render inside calendar views. |
📄 PDF or Email Content Blocks | Render sections of invoices, tables, or dynamic emails consistently. |
📋 Form Elements | Input fields, labels, and validations encapsulated inside reusable components. |
💡 Tip: If you’re copying the same ERB partial more than twice, it’s a perfect case for a ViewComponent.
🧩 Gems & Libraries for ViewComponent Pattern
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
view_component | Official gem by GitHub for building reusable, testable, and encapsulated UI components in Rails using Ruby + templates. | Main gem to implement ViewComponent pattern. |
view_component-contrib | A set of community-powered tools and helpers to enhance ViewComponent development (e.g. slots helpers, content_for, etc.). | When you need extra utilities like slot helpers or testing DSL extensions. |
lookbook | An interactive UI component preview and documentation tool (like Storybook for ViewComponent). | Preview, develop, and document ViewComponents visually. |
rspec-rails | Used to write unit tests for ViewComponents using render_inline . | Test your components in isolation with input/output checks. |
capybara | Used for integration testing of component HTML output and DOM behavior. | Great for checking rendered content and accessibility. |
tailwindcss-rails | Common styling library that pairs well with ViewComponents for utility-first CSS in components. | To keep styles consistent and isolated per component. |
🛠️ Best Implementation – ViewComponent in Rails (Step-by-Step)
This example builds a reusable AlertComponent for displaying success/error/info messages across your app.
📦 Step 1: Install the Gem
# In Gemfile
gem "view_component", require: "view_component/engine"
bundle install
This will auto-create support for app/components
and ViewComponent testing.
📁 Step 2: Generate a Component
bin/rails generate component Alert message type
This creates:
app/components/alert_component.rb
app/components/alert_component.html.erb
🧠 Step 3: Add Logic to the Component
# app/components/alert_component.rb
class AlertComponent < ViewComponent::Base
def initialize(message:, type: :info)
@message = message
@type = type
end
end
🎨 Step 4: Create the Template
<!-- app/components/alert_component.html.erb -->
<div class="alert alert-<%= @type %>">
<%= @message %>
</div>
This allows dynamic messages and alert types (e.g., success, error).
🖼️ Step 5: Render the Component in a View
<%= render(AlertComponent.new(message: "Saved successfully!", type: :success)) %>
✅ Use it anywhere you want consistent alert UI.
🧪 Step 6: Test the Component
File: spec/components/alert_component_spec.rb
require "rails_helper"
RSpec.describe AlertComponent, type: :component do
it "renders a success message" do
render_inline(AlertComponent.new(message: "Saved!", type: :success))
expect(page).to have_css(".alert-success", text: "Saved!")
end
end
👁️ Step 7: Add a Preview (Optional)
File: test/components/previews/alert_component_preview.rb
class AlertComponentPreview < ViewComponent::Preview
def success
render(AlertComponent.new(message: "Saved!", type: :success))
end
def error
render(AlertComponent.new(message: "Something went wrong!", type: :danger))
end
end
✅ Visit /rails/view_components
to preview all components (when preview is enabled).
🧩 Benefits of This Implementation
- ✅ Reusable: One component, multiple alert types
- ✅ Testable: Pure Ruby logic, clean template separation
- ✅ Encapsulated: No need for partials or global helper methods
- ✅ Design System Friendly: Works great with Tailwind or Bootstrap
💡 Example
app/components/alert_component.rb
class AlertComponent < ViewComponent::Base
def initialize(message:, type: :info)
@message = message
@type = type
end
end
app/components/alert_component.html.erb
<div class="alert alert-<%= @type %>">
<%= @message %>
</div>
Usage in view:
<%= render(AlertComponent.new(message: "Saved!", type: :success)) %>
🔁 Alternative Concepts
- Partials: Reusable HTML snippets but lack testability and encapsulation.
- Helpers: Good for logic but not for rendering markup.
- Presenters: Help format data, but do not render full views like components.
🛠️ Technical Questions & Answers – ViewComponent
Q1: What is ViewComponent in Rails?
A: ViewComponent is a Ruby gem (by GitHub) that allows you to build reusable, testable UI components in Rails. Each component has a Ruby class and a corresponding HTML template.
# app/components/alert_component.rb
class AlertComponent < ViewComponent::Base
def initialize(message:, type: :info)
@message = message
@type = type
end
end
<!-- alert_component.html.erb -->
<div class="alert alert-<%= @type %>">
<%= @message %>
</div>
Q2: How is ViewComponent different from partials?
A: Partials are templates only. ViewComponents have structure (Ruby class), logic, and tests.
Partials | ViewComponents |
---|---|
No Ruby logic | Ruby + Template |
Harder to test | Fully testable |
Scattered view logic | Encapsulated UI |
Q3: How do you render a ViewComponent?
A: Use the render
method in your views:
<%= render(AlertComponent.new(message: "Hello!", type: :info)) %>
Q4: Can you pass blocks (slots) to components?
A: Yes! You can pass nested content using slots:
# app/components/card_component.rb
class CardComponent < ViewComponent::Base
renders_one :header
renders_one :body
end
<%= render(CardComponent.new) do |c| %>
<% c.header { "Card Title" } %>
<% c.body { "This is the content." } %>
<% end %>
Q5: How do you test a component?
A: Use render_inline
in RSpec:
RSpec.describe AlertComponent, type: :component do
it "renders a message" do
render_inline(AlertComponent.new(message: "Test", type: :info))
expect(page).to have_content("Test")
end
end
Q6: Can ViewComponents be previewed in development?
A: Yes! You can generate previews:
# test/components/previews/alert_component_preview.rb
class AlertComponentPreview < ViewComponent::Preview
def default
render(AlertComponent.new(message: "Preview!", type: :info))
end
end
Then visit /rails/view_components
to view them.
Q7: Can components render inside each other?
A: Yes. You can nest components:
<%= render(CardComponent.new) do |c| %>
<% c.body do %>
<%= render(ButtonComponent.new(text: "Click")) %>
<% end %>
<% end %>
Q8: What are the benefits of using ViewComponent?
- ✅ Testable, isolated components
- ✅ Reusable UI blocks
- ✅ Better structure and readability
- ✅ Cleaner separation of logic and markup
Q9: Can you use ViewComponent with Tailwind or Bootstrap?
A: Yes. Since ViewComponents render HTML, you can apply any CSS framework or utility class.
Q10: Should ViewComponents replace all partials?
A: Not always. Use ViewComponents when you need logic, reusability, or tests. Use partials for extremely simple templates.
✅ Best Practices with Examples – ViewComponent
1. ✅ Use clear naming with the `Component` suffix
Keep component classes obvious and consistent:
# Good
class ButtonComponent < ViewComponent::Base
# Bad
class Btn < ViewComponent::Base
2. ✅ Keep logic in the Ruby class, not in the template
This makes your views cleaner and easier to test.
# app/components/alert_component.rb
def status_class
case @type
when :success then "alert-success"
when :error then "alert-danger"
else "alert-info"
end
end
<!-- alert_component.html.erb -->
<div class="alert <%= status_class %>"><%= @message %></div>
3. ✅ Use renders_one
and renders_many
for slots
This allows flexible and readable components:
renders_one :header
renders_many :items
4. ✅ Write tests for every component
ViewComponents are meant to be testable — use render_inline
in RSpec.
it "renders with message" do
render_inline(AlertComponent.new(message: "Hi!", type: :success))
expect(page).to have_content("Hi!")
end
5. ✅ Use component previews in development
This helps you see components in isolation and test variations visually.
# test/components/previews/button_component_preview.rb
class ButtonComponentPreview < ViewComponent::Preview
def primary
render(ButtonComponent.new(label: "Save", style: :primary))
end
end
6. ✅ Avoid using instance variables outside the component
All logic should be self-contained. Use arguments, not globals.
7. ✅ Use TailwindCSS or similar inside components
Keep styling declarative and scoped inside the template.
<button class="btn bg-blue-500 hover:bg-blue-600 text-white">
<%= @label %>
</button>
8. ✅ Organize components into folders if needed
Group them by domain, just like controllers or models.
app/components/
├── buttons/
│ └── primary_button_component.rb
├── alerts/
│ └── alert_component.rb
9. ✅ Reuse components inside other components
You can compose components for complex UI blocks:
<%= render(ButtonComponent.new(label: "Edit")) %>
10. ✅ Don’t over-componentize
Use ViewComponent for reusable or logic-heavy UIs. Use partials for one-time, static chunks.
🌍 Real-World Use Cases
- ⚠️ Alert boxes and notifications
- 🧩 Card components for dashboards
- 📦 Product listings in e-commerce views
- 📅 Calendars and schedule widgets
- 📄 PDF-renderable sections
- 🔁 Reusable form groups and inputs
Interactor Pattern in Rails
🧠 Detailed Explanation
The Interactor Pattern is a way to organize one specific task (or action) in your Rails app into its own file, instead of putting that logic in controllers or models.
🎯 What is an Interactor?
An interactor is just a Ruby class that:
- 👔 Does one job (like “Create a user” or “Send an email”)
- 📦 Takes input (like form data)
- ✅ Returns success or failure
- 📤 Shares results with the controller or another interactor
📦 What does it look like?
# app/interactors/create_user.rb
class CreateUser
include Interactor
def call
user = User.new(context.params)
if user.save
UserMailer.welcome(user).deliver_later
context.user = user
else
context.fail!(error: user.errors.full_messages.to_sentence)
end
end
end
In your controller:
result = CreateUser.call(params: user_params)
if result.success?
redirect_to result.user
else
flash[:alert] = result.error
render :new
end
🤔 Why use it?
- ✅ Keeps controllers clean (no long methods)
- ✅ Keeps models focused on data, not process
- ✅ Makes business logic reusable and testable
- ✅ Makes complex actions easy to follow
🧠 Think of it like:
- 🧾 A to-do list handler: it takes an action and completes it step-by-step.
- 📤 A mailroom clerk: it gets something to do, runs it, and hands back the result.
- 🔌 A plugin: plug in any action like create user, place order, or notify admin.
In short: An interactor handles one action. It keeps your app tidy and your logic reusable.
📘 Key Terms & Concepts – Interactor Pattern
🔖 Term | 📄 Description |
---|---|
Interactor | A Ruby class that performs a single business task (like creating a user or processing an order). |
Interactor Pattern | An architectural pattern where each piece of business logic is encapsulated in its own class (a single job per class). |
include Interactor | Adds methods like call and context to your class, making it an official interactor. |
call method | The method where the logic is placed. This is run when you use ClassName.call(args) . |
Context | A shared object that stores input and output. Use context.params to read, context.user = user to write. |
context.fail! | Stops execution and marks the result as a failure. Optionally takes an error message (e.g., context.fail!(error: ...) ). |
Success? | After running, use result.success? or result.failure? to check the outcome. |
Interactor::Organizer | A special interactor that can call multiple interactors in sequence. Each one shares the same context. |
Single Responsibility | Each interactor must do only one thing — no mixing of responsibilities or side effects. |
Fail Fast | If something goes wrong, stop immediately using fail! — don’t continue execution. |
Testable Services | Since each interactor is just a Ruby class with one method, it’s easy to test in isolation. |
🔁 Flow – How the Interactor Pattern Works
- 1. You create an interactor class
Place it inapp/interactors
and define acall
method. - 2. You include
Interactor
in the class
This gives your class access tocontext
andfail!
. - 3. You pass input into
call
using a hash
The input becomes part of the sharedcontext
. - 4. You execute business logic inside the
call
method
If successful, you save results incontext
. - 5. You return the result and check
success?
orfailure?
This helps you handle success or error in the controller or another interactor.
# Step-by-step example:
result = CreateOrder.call(user: current_user, params: order_params)
if result.success?
redirect_to result.order
else
render :new, alert: result.error
end
📌 Where & How to Use Interactor Pattern in Rails
Use the Interactor Pattern anywhere you have multi-step business logic that shouldn’t live in controllers or models.
📍 Use Case | 💡 Description |
---|---|
👤 User Registration | Create a user, send a welcome email, track event → all in one interactor. |
📦 Order Checkout | Charge the customer, save order, send receipt — each as a single interactor or an organized chain. |
📧 Email Notifications | Send automated emails after certain triggers, without cluttering controllers. |
🛠 Background Jobs | Complex Sidekiq jobs use interactors for separation of logic. |
🔁 Data Imports | Validate CSV, insert records, handle rollback using fail! . |
🔗 Organizer Flows | Use Interactor::Organizer to run a series of interactors in a chain (e.g., Verify → Create → Notify). |
💡 Rule of Thumb: If a method has more than 2–3 responsibilities or conditionals, move it to an interactor.
🧩 Gems & Libraries – Interactor Pattern in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
interactor | The official gem by Collective Idea for implementing the Interactor pattern. Adds support for `call`, `context`, `fail!`, and `Organizer`. | Encapsulate business logic in reusable classes. |
dry-monads | A gem from dry-rb that introduces result objects like `Success()` and `Failure()`. Can be used as an alternative to context. | Functional-style service results without `context`. |
trailblazer-operation | Part of the Trailblazer framework. Offers advanced, strict Interactor-like “operation” classes with contracts and steps. | For large apps needing full-blown business orchestration. |
light-service | A minimalist alternative to Interactor. Encourages service classes with a `call` method and a context hash. | Simplified service logic with fail-fast support. |
rspec-rails | Useful for testing interactors with success/failure expectations. | Testing each interactor’s outcomes independently. |
sidekiq | Although not an interactor tool, it’s often used with interactors to run business logic in background jobs. | Run interactors asynchronously in background jobs. |
🛠️ Best Implementation – Interactor Pattern in Rails
Let’s build a real-world example: CreateUser interactor that registers a user, sends a welcome email, and handles failure gracefully.
📦 Step 1: Install the interactor gem
# In your Gemfile
gem 'interactor'
bundle install
📁 Step 2: Create the Interactor class
Generate and place interactors in app/interactors/
.
# app/interactors/create_user.rb
class CreateUser
include Interactor
def call
user = User.new(context.params)
if user.save
UserMailer.welcome_email(user).deliver_later
context.user = user
else
context.fail!(error: user.errors.full_messages.to_sentence)
end
end
end
Highlights:
- 🎯 Uses
context.params
to access input - 📩 Uses
context.user
to return output - 🚫 Uses
context.fail!
for graceful failure
🖼️ Step 3: Use the Interactor in your controller
# app/controllers/users_controller.rb
def create
result = CreateUser.call(params: user_params)
if result.success?
redirect_to result.user, notice: "User created!"
else
flash[:alert] = result.error
render :new
end
end
Why it’s better: The controller stays clean, and logic is easy to test separately.
🧪 Step 4: Test the Interactor
# spec/interactors/create_user_spec.rb
require 'rails_helper'
RSpec.describe CreateUser, type: :interactor do
let(:valid_params) { { name: "Ali", email: "ali@example.com", password: "123456" } }
let(:invalid_params) { { name: "", email: "" } }
it "creates a user with valid params" do
result = CreateUser.call(params: valid_params)
expect(result).to be_success
expect(result.user).to be_persisted
end
it "fails with invalid params" do
result = CreateUser.call(params: invalid_params)
expect(result).to be_failure
expect(result.error).to be_present
end
end
🔗 Step 5 (Optional): Use an Organizer to group steps
# app/interactors/register_user.rb
class RegisterUser
include Interactor::Organizer
organize CreateUser, SendWelcomeGift, TrackSignupAnalytics
end
💡 Each interactor in the chain shares the same context
.
✅ Summary
- 🚀 Easy to read and test
- 🔁 Reusable in other workflows
- 🧹 Keeps controllers and models clean
- ❌ No more fat models or long controller methods
💡 Example
app/interactors/create_user.rb
class CreateUser
include Interactor
def call
user = User.new(context.params)
if user.save
UserMailer.welcome(user).deliver_later
context.user = user
else
context.fail!(error: user.errors.full_messages.to_sentence)
end
end
end
Usage in controller:
result = CreateUser.call(params: user_params)
if result.success?
redirect_to result.user
else
flash[:alert] = result.error
render :new
end
🔁 Alternative Methods or Concepts
- Fat Models: Logic directly inside models (bad for readability).
- Command Objects: Similar to interactors but often lack context and fail! pattern.
- Service Objects: Manually built classes that perform a single operation.
🛠️ Technical Q&A – Interactor Pattern
Q1: What is the Interactor Pattern in Rails?
A: The Interactor Pattern encapsulates a single business action (e.g., create a user, charge a card) in a dedicated Ruby class. It keeps controllers and models clean and makes logic reusable and testable.
Q2: What gem is used to implement it?
A: The interactor
gem by Collective Idea provides a DSL (`context`, `fail!`, `call`) for building interactors.
Q3: How do you define an interactor?
A: Create a class with `include Interactor`, then define the `call` method.
class CreateUser
include Interactor
def call
user = User.new(context.params)
context.user = user if user.save
context.fail!(error: "Invalid user") unless user.persisted?
end
end
Q4: What is context
in an interactor?
A: It’s a shared object used to receive inputs and return results.
context.params
→ inputcontext.user
→ outputcontext.success?
/context.failure?
→ status check
Q5: What does context.fail!
do?
A: It stops the interactor and marks the result as a failure. Execution after it is skipped.
context.fail!(error: "Email missing") if context.params[:email].blank?
Q6: How do you use an interactor in a controller?
A:
result = CreateUser.call(params: user_params)
if result.success?
redirect_to result.user
else
render :new, alert: result.error
end
Q7: How do you test an interactor?
A: You call the interactor and assert on the result’s success/failure and output values.
RSpec.describe CreateUser do
it "creates a user" do
result = described_class.call(params: { name: "Ali" })
expect(result).to be_success
expect(result.user).to be_persisted
end
end
Q8: What is an Organizer in the interactor gem?
A: An Organizer
is a way to group multiple interactors into one workflow. They all share the same context
.
class SignupFlow
include Interactor::Organizer
organize CreateUser, SendWelcomeEmail, LogAnalytics
end
Q9: Can you pass multiple values into an interactor?
A: Yes. You pass them as a hash to call
. They’re accessible via context
.
CreateUser.call(params: user_params, current_user: admin_user)
Q10: When should you use an interactor vs a service object?
A: Use an interactor when you want structured failure handling, shared context, and reusable workflow composition. Use a service object for simple one-liners or low logic tasks.
✅ Best Practices with Examples – Interactor Pattern
1. ✅ Name your interactor after the action it performs
Use clear, verb-based names like CreateUser
, SendInvoice
, or CancelOrder
.
# Good
class CreateUser
# Bad
class UserService
2. ✅ Keep logic inside call
only
All interactor logic must be placed inside the call
method. Avoid defining unrelated methods unless you extract private helpers.
def call
context.user = User.create(context.params)
end
3. ✅ Use context
to pass and return data
Use context
like a structured input/output hash:
def call
context.user = User.new(context.params)
end
4. ✅ Use context.fail!
to exit early on error
Fail fast and avoid nested if
conditions.
def call
context.fail!(error: "Email missing") if context.params[:email].blank?
end
5. ✅ Keep each interactor focused on a single task
Each interactor should do one job — not a mix of DB write, email, and analytics.
# ✅ CreateUser
# ✅ SendWelcomeEmail
# ✅ TrackSignup
6. ✅ Chain complex workflows using Interactor::Organizer
This keeps each interactor simple while allowing step-by-step orchestration.
class SignupFlow
include Interactor::Organizer
organize CreateUser, SendWelcomeEmail, TrackSignupAnalytics
end
7. ✅ Use context.success?
and context.failure?
in consumers
Don’t rely on return values — always check status.
result = CreateUser.call(params: user_params)
if result.success?
redirect_to result.user
else
flash[:alert] = result.error
end
8. ✅ Write unit tests for every interactor
Interactor logic is isolated and easy to test:
it "fails when email is blank" do
result = CreateUser.call(params: { email: "" })
expect(result).to be_failure
end
9. ✅ Keep side effects (emails, jobs) isolated
For example, send email in a separate SendWelcomeEmail
interactor, not inside CreateUser
.
10. ✅ Keep interactors in app/interactors
Maintain consistent structure to organize them clearly:
app/
├─ interactors/
│ ├─ create_user.rb
│ ├─ send_welcome_email.rb
│ └─ register_user.rb
🌍 Real-World Scenarios
- 👤 User sign-up with onboarding steps
- 📦 Order processing and payment handling
- 📧 Sending notifications after events
- 🔁 Importing/exporting data pipelines
- 📊 Background jobs with business logic
Repository Pattern in Rails
🧠 Detailed Explanation
The Repository Pattern is a way to organize your code so that all the database queries and logic for a model are stored in one place — called a “repository.”
This helps you:
- ✅ Keep your controllers and services clean
- ✅ Avoid repeating queries everywhere
- ✅ Easily swap or change how data is fetched later
- ✅ Test your business logic without touching the database
📦 Simple Example
Before (bad): You put queries in the controller
# In controller
@users = User.where(active: true).order(:created_at)
After (good): You move that logic to a repository
# app/repositories/user_repository.rb
class UserRepository
def self.active_users
User.where(active: true).order(:created_at)
end
end
# In controller
@users = UserRepository.active_users
🎯 What goes inside a Repository?
- 📥 Query logic (e.g., filters, finders)
- 📤 Data manipulation (e.g., create, update, delete)
- 🔎 Aggregations or custom searches
🧠 Think of it like:
- 📚 A library for all data-related tasks for one model
- 🔌 A plug-in between your app and the database
- 🧹 A way to keep your code clean, dry, and organized
In short: The Repository Pattern gives you one central place to handle all data access for a model, so your app stays clean and easy to manage.
📘 Key Terms & Concepts – Repository Pattern
🔖 Term | 📄 Description |
---|---|
Repository Pattern | A design pattern that stores all query and data access logic for a model in one place — the repository class. |
Repository | A Ruby class (e.g., UserRepository ) that contains methods to interact with the database for a specific model. |
Abstraction | Hiding the details of how data is fetched or stored, so the rest of your app doesn’t need to care how queries are built. |
Encapsulation | Keeping all related query logic together inside a single class, instead of spreading it across controllers and services. |
Single Responsibility | Each repository should only handle one thing: database access for its specific model. |
Thin Controllers | Controllers should not contain business logic or raw queries. Using repositories helps keep them clean and focused on flow. |
Centralized Query Logic | A repository centralizes all ActiveRecord calls for a model, so queries don’t repeat in different files. |
Testability | Repositories make it easier to stub or mock queries in tests, improving speed and isolation. |
DRY Principle | Don’t Repeat Yourself — repositories reduce duplication of query logic across controllers and services. |
Flexible Data Source | Since repositories abstract queries, you can swap ActiveRecord with another ORM or API without changing the app logic. |
🔁 Flow – How the Repository Pattern Works
- 1. Create a Repository Class
Place it inapp/repositories/
, e.g.,UserRepository
. - 2. Add query methods inside the class
Use class methods to define operations likefind_by_email
oractive_users
. - 3. Replace raw queries in controllers/services
Instead of writing queries directly in your app code, call the repository method. - 4. Use repository methods across the app
Call them from controllers, services, jobs, mailers, etc. - 5. Refactor and test easily
Update or test logic in one place without touching multiple files.
Example:
# app/repositories/user_repository.rb
class UserRepository
def self.recent(limit = 10)
User.order(created_at: :desc).limit(limit)
end
end
# app/controllers/dashboard_controller.rb
@users = UserRepository.recent(5)
📌 Where & How to Use the Repository Pattern in Rails
The Repository Pattern is perfect for organizing and reusing query logic across your Rails app.
📍 Area | 💡 How It’s Used |
---|---|
🧠 Controllers | Replace raw ActiveRecord queries with clean method calls (e.g., UserRepository.active ). |
⚙️ Services | Use repository methods to fetch or persist data in service objects (e.g., OrderRepository.pending_today ). |
📬 Jobs / Workers | Fetch data for background processing (e.g., sending emails, updating stats). |
📈 Reports & Exports | Generate filtered or aggregated data for analytics using a dedicated repository method. |
🔁 Shared Logic | Use in multiple places (e.g., filter users for admin panel, dashboard, and API) without repeating query logic. |
🧪 Tests | Stub repository methods to avoid hitting the DB directly in unit tests. |
✅ Tip: If you’re copying or repeating a query more than twice — it’s a sign you should move it into a repository method.
🧩 Gems & Libraries – Repository Pattern in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
ActiveRecord | Built-in Rails ORM used under the hood in most repositories for querying the database. | Primary data source for repositories in Rails apps. |
rom-rb | ROM (Ruby Object Mapper) is a flexible mapping and persistence library that supports custom repositories and data layers. | Advanced data abstraction beyond ActiveRecord, good for complex, layered apps. |
dry-struct | Provides immutable value objects. Often used with ROM repositories to return structured data instead of AR models. | Returning structured data from repositories. |
dry-container | Used to register repositories and auto-load them like services. Encourages separation of concerns. | Dependency injection and clean registration of repositories. |
rails-config | Stores configuration values for repositories (like limits, query timeouts, or endpoints). | Manage settings that influence repository behavior. |
factory_bot | Used in tests to create model data and test repository behavior in isolation. | Testing repository input/output without hitting real services. |
🛠️ Best Implementation – Repository Pattern in Rails (Step-by-Step)
Let’s build a repository for the User model. It will handle common queries like fetching active users, finding by email, and paginating users.
📁 Step 1: Create the Repository Directory
Create a folder in your app to keep repositories organized:
# Terminal
mkdir app/repositories
📦 Step 2: Create the Repository Class
# app/repositories/user_repository.rb
class UserRepository
def self.all
User.all
end
def self.find_by_email(email)
User.find_by(email: email)
end
def self.active_users
User.where(active: true).order(created_at: :desc)
end
def self.paginated(page:, per_page:)
User.order(created_at: :desc).page(page).per(per_page)
end
def self.created_after(date)
User.where("created_at > ?", date)
end
end
🧠 Each method is clean, focused, and reusable.
🧪 Step 3: Use the Repository in Your Controller
# app/controllers/users_controller.rb
def index
@users = UserRepository.paginated(page: params[:page], per_page: 20)
end
def show
@user = UserRepository.find_by_email(params[:email])
end
✅ Keeps the controller lean and readable.
🧪 Step 4: Add RSpec Test for the Repository
# spec/repositories/user_repository_spec.rb
require 'rails_helper'
RSpec.describe UserRepository do
let!(:user1) { create(:user, email: "a@example.com", active: true) }
let!(:user2) { create(:user, email: "b@example.com", active: false) }
it "finds user by email" do
result = described_class.find_by_email("a@example.com")
expect(result).to eq(user1)
end
it "returns only active users" do
results = described_class.active_users
expect(results).to include(user1)
expect(results).not_to include(user2)
end
end
🎯 Easy to test each query in isolation.
📘 Step 5: Follow Naming & Structure Conventions
UserRepository
→ Handles allUser
queries- Use
self.method
for reusability - Avoid adding business logic — only query & fetch logic
- Keep method names expressive like
active_users
,created_after
🔗 Bonus: Inject Repositories in Services
# app/services/report_service.rb
class ReportService
def initialize(user_repo = UserRepository)
@user_repo = user_repo
end
def recent_signups
@user_repo.created_after(7.days.ago)
end
end
💡 This makes the service testable and decoupled from ActiveRecord.
✅ Summary
- 🏗 Keep all queries for one model in its own repository
- 🧹 Makes controllers, services, and jobs cleaner
- 🔁 Encourages reuse and testability
- 🧪 Easy to test without touching controllers or the DB layer directly
💡 Example
app/repositories/user_repository.rb
class UserRepository
def self.active_users
User.where(active: true)
end
def self.find_by_email(email)
User.find_by(email: email)
end
def self.create_user(attrs)
User.create(attrs)
end
end
Usage:
users = UserRepository.active_users
admin = UserRepository.find_by_email("admin@example.com")
🔁 Alternative Patterns
- Service Objects: Good for actions, but not for encapsulating query logic.
- Query Objects: Ideal for complex searches and filters.
- Concerns/Scopes: Keep logic in models but can lead to bloated files.
🛠️ Technical Q&A – Repository Pattern in Rails
Q1: What is the Repository Pattern?
A: The Repository Pattern separates the database query logic from the rest of your application. It provides a clean API for accessing data while hiding the details of the ORM (e.g., ActiveRecord).
Q2: Why should I use a repository instead of putting queries in controllers?
A: Repositories help you avoid repetition, keep controllers lean, isolate data access for testing, and improve maintainability.
# Controller with repo:
@users = UserRepository.active_users
Q3: What should be inside a repository class?
A: Only query-related logic: finding, filtering, sorting, and fetching records.
class ProductRepository
def self.latest(limit = 5)
Product.order(created_at: :desc).limit(limit)
end
end
Q4: Can repositories return ActiveRecord objects?
A: Yes. In standard Rails, repositories usually return ActiveRecord objects or collections.
user = UserRepository.find_by_email("admin@example.com")
Q5: How do you write tests for repositories?
A: Use RSpec or Minitest to test inputs and expected output from repository methods.
RSpec.describe UserRepository do
it "returns only active users" do
create(:user, active: true)
create(:user, active: false)
expect(UserRepository.active_users.count).to eq(1)
end
end
Q6: How does a repository improve testability?
A: You can mock/stub repository methods in services or controllers instead of relying on real DB records, making tests faster and more isolated.
allow(UserRepository).to receive(:find_by_email).and_return(mock_user)
Q7: How does a repository differ from a service object?
A: A repository handles **data access**, while a service object performs **business logic or actions**.
# Repository
OrderRepository.pending_today
# Service
OrderProcessor.new(order).process
Q8: Where do I store repository classes?
A: Place them in app/repositories
. You can namespace them if needed (e.g., Admin::UserRepository
).
Q9: Can a repository access multiple models?
A: Yes, but only if it’s for cross-related query logic. For example, a ReportRepository
might pull data from User
, Order
, and Product
.
Q10: Is the Repository Pattern required in Rails?
A: No, it’s optional. Rails encourages “fat models, skinny controllers”, but using repositories adds better structure to medium and large apps.
✅ Best Practices with Examples – Repository Pattern in Rails
1. ✅ Keep one repository per model
Each repository should be responsible for a single model (e.g., UserRepository
for User
).
# Good
class UserRepository
def self.active
User.where(active: true)
end
end
2. ✅ Avoid business logic in repositories
Repositories are only for data access. Put business rules in service objects.
# 🚫 Don't
UserRepository.promote_user_to_admin(user)
# ✅ Do
UserPromoter.new(user).call
3. ✅ Keep repository methods short and descriptive
Name methods clearly based on what they return.
def self.registered_last_7_days
User.where("created_at > ?", 7.days.ago)
end
4. ✅ Use repositories in controllers, services, jobs
This removes the need for repeated queries across the app.
# Instead of repeating:
User.where(active: true)
# Use:
UserRepository.active
5. ✅ Support flexible inputs using keyword args
Make methods reusable and clear to call.
def self.paginated(page:, per:)
User.order(:created_at).page(page).per(per)
end
6. ✅ Return ActiveRecord relations when chaining is useful
This allows callers to further filter if needed.
def self.active
User.where(active: true)
end
# Controller
UserRepository.active.limit(10)
7. ✅ Keep repositories testable and isolated
Test them directly with RSpec and use factories for input.
RSpec.describe UserRepository do
it "finds user by email" do
user = create(:user, email: "test@example.com")
result = UserRepository.find_by_email("test@example.com")
expect(result).to eq(user)
end
end
8. ✅ Namespace repositories for subdomains if needed
For large apps, use namespaced repositories for separation.
Admin::UserRepository.recent
9. ✅ Use dependency injection in services
This makes services more flexible and testable.
class ReportService
def initialize(user_repo = UserRepository)
@user_repo = user_repo
end
end
10. ✅ Document expected input/output for complex methods
Especially if your method takes multiple filters or conditions.
# Returns users created within a given time range
def self.created_between(start_date, end_date)
User.where(created_at: start_date..end_date)
end
🌍 Real-World Use Cases
- 👥 Managing user filters and paginated queries
- 📦 Inventory queries in warehouse management
- 📊 Complex report generation logic
- 🔗 Aggregating data from multiple models for APIs
- 🔍 Multi-condition search and export modules
Command Pattern in Rails
🧠 Detailed Explanation
The Command Pattern is a way to wrap a specific action — like “create a user” or “send an email” — inside its own Ruby class. This pattern turns that action into a command object that can be reused, tested, and passed around your app.
🎯 What does it mean in Rails?
In Rails, you might have logic in your controller like this:
# Not clean — logic inside controller
def create
user = User.new(params[:user])
if user.save
UserMailer.welcome(user).deliver_later
redirect_to user
else
render :new
end
end
With the Command Pattern, you move all of that logic into a plain Ruby class:
# app/commands/create_user_command.rb
class CreateUserCommand
def initialize(params)
@params = params
end
def call
user = User.new(@params)
if user.save
UserMailer.welcome(user).deliver_later
user
else
raise StandardError, user.errors.full_messages.to_sentence
end
end
end
Now the controller is clean:
def create
@user = CreateUserCommand.new(user_params).call
redirect_to @user
rescue StandardError => e
flash[:alert] = e.message
render :new
end
🤔 Why use it?
- ✅ Keeps your controllers clean
- ✅ Makes actions reusable across jobs, services, or CLI tasks
- ✅ Easier to test because logic is isolated
- ✅ Supports undo/redo or queuing logic (like background jobs)
🧠 Think of it like:
A command object is like a task card:
- ✍️ You write down the task (e.g., “create user”)
- 📦 Pass it to someone (controller, job, etc.)
- ✅ They run the task using one consistent method:
call
In short: The Command Pattern lets you package logic into a single, clean, reusable object with one job.
📘 Key Terms & Concepts – Command Pattern
🔖 Term | 📄 Description |
---|---|
Command Pattern | A behavioral pattern where actions are wrapped in their own objects, allowing them to be reused, queued, or undone. |
Command Object | A plain Ruby class that performs a specific business action (e.g., CreateUserCommand). |
Invoker | The part of the app that calls the command. In Rails, this is usually a controller, job, or service. |
Receiver | The model or system that the command interacts with (e.g., User , Mailer , etc.). |
call method | The main method inside the command object that performs the task. It acts like a “run” button. |
Single Responsibility | Each command object should do one thing only. This keeps it easy to test and maintain. |
Encapsulation | The command hides how the action works internally. Consumers don’t need to know what it does — just how to run it. |
Reusability | Commands can be reused in multiple places like controllers, background jobs, rake tasks, or APIs. |
Testability | Commands are easy to test since they’re plain Ruby classes with a single method and no Rails dependencies. |
Undo/Redo (Advanced) | Some command patterns allow storing history and reversing actions. Rarely used in Rails, but supported in theory. |
🔁 Flow – How the Command Pattern Works in Rails
- 1. Create a Command Class
A plain Ruby class placed inapp/commands/
(e.g.,CreateUserCommand
). - 2. Define an
initialize
method
It receives the inputs your command needs (e.g., user params). - 3. Define the
call
method
This contains the logic to execute the command. - 4. Use the command in controllers, jobs, or services
Instead of inline logic, simply call.new(...).call
. - 5. Return result or raise error
Commands usually return an object or raise if something goes wrong.
🔧 Example:
# app/commands/create_user_command.rb
class CreateUserCommand
def initialize(params)
@params = params
end
def call
user = User.new(@params)
raise StandardError, "Invalid user" unless user.save
UserMailer.welcome(user).deliver_later
user
end
end
Usage:
# In controller
@user = CreateUserCommand.new(user_params).call
📌 Where & How to Use the Command Pattern in Rails
Use the Command Pattern for any action that:
- ✅ Involves multiple steps (e.g., saving + emailing)
- ✅ May be reused in different places (e.g., controller + background job)
- ✅ Requires clean separation of concerns
📍 Use Case | 💡 How It’s Used |
---|---|
👤 User Signup | Create user → send welcome email → track analytics |
📦 Order Placement | Create order → deduct stock → send confirmation |
📧 Email Campaign | Loop over customers and send dynamic messages |
📊 Report Export | Generate report → save CSV → email to admin |
💳 Payment Handling | Charge customer → update invoice → notify team |
✅ Tip: Use the Command Pattern when your controller method feels too long or is doing more than one thing.
🧩 Gems & Libraries – Command Pattern in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
dry-monads | Provides functional result objects like Success and Failure . Useful for returning structured results from commands. | Improves clarity and flow control in command objects. |
interactor | Encapsulates business logic into “interactors” (command-like objects). Supports context , fail! , and Organizer . | Builds command-style classes with built-in error handling. |
trailblazer-operation | Part of the Trailblazer architecture. Provides highly structured command-like “operations” with validations, steps, and contracts. | Large apps with layered command workflow logic. |
dry-container | Used to register and access command classes using dependency injection. | Organize and autoload command objects across layers. |
sidekiq | While not a command gem, it works well with commands by executing them as background jobs (e.g., SendWelcomeEmailCommand ). | Run command objects asynchronously. |
command_kit | A gem for building command-line command objects. Not Rails-specific but follows the same principles. | Build CLI-style commands for Rails scripts. |
🛠️ Best Implementation – Command Pattern in Rails (Step-by-Step)
Let’s walk through a complete example: CreateUserCommand — a command object that creates a user and sends a welcome email.
📁 Step 1: Create Command Directory
Place your command objects in a dedicated folder:
# Terminal
mkdir app/commands
📦 Step 2: Build the Command Class
Each command has:
- Constructor: receives dependencies/params
call
method: executes the action
# app/commands/create_user_command.rb
class CreateUserCommand
def initialize(params)
@params = params
end
def call
user = User.new(@params)
if user.save
UserMailer.welcome_email(user).deliver_later
return user
else
raise StandardError, user.errors.full_messages.to_sentence
end
end
end
🧼 Step 3: Use it in the Controller
# app/controllers/users_controller.rb
def create
@user = CreateUserCommand.new(user_params).call
redirect_to @user, notice: "User created!"
rescue StandardError => e
flash[:alert] = e.message
render :new
end
🎯 Now your controller is short and readable!
🧪 Step 4: Add RSpec Tests
# spec/commands/create_user_command_spec.rb
require 'rails_helper'
RSpec.describe CreateUserCommand do
let(:valid_params) { { name: "Ali", email: "ali@example.com", password: "123456" } }
let(:invalid_params) { { name: "", email: "invalid" } }
it "creates a user successfully" do
user = described_class.new(valid_params).call
expect(user).to be_persisted
end
it "raises error with invalid data" do
expect {
described_class.new(invalid_params).call
}.to raise_error(StandardError, /Email/)
end
end
🧪 Optional: Use with Sidekiq for Background Execution
# app/jobs/create_user_job.rb
class CreateUserJob < ApplicationJob
queue_as :default
def perform(params)
CreateUserCommand.new(params).call
end
end
# Usage
CreateUserJob.perform_later(params)
📘 Naming & Structure Guidelines
- ✅ Name command like an action (e.g.,
CreateUserCommand
,SendInvoiceCommand
) - ✅ Store in
app/commands
- ✅ Keep the
call
method short and focused - ✅ Raise errors instead of silently failing
- ✅ Reuse commands in services, jobs, or CLI
✅ Summary
- 🚀 Isolates logic into testable units
- ♻️ Reusable across controllers, jobs, services
- 🧹 Keeps your MVC clean and modular
- 🧪 Simple to test using RSpec
💡 Example
app/commands/create_user_command.rb
class CreateUserCommand
def initialize(params)
@params = params
end
def call
user = User.new(@params)
if user.save
UserMailer.welcome(user).deliver_later
user
else
raise StandardError, user.errors.full_messages.to_sentence
end
end
end
Usage:
user = CreateUserCommand.new(user_params).call
🔁 Alternative Concepts
- Interactor Pattern: Similar, but uses context and fail! for structured results.
- Service Objects: Can serve as simple command-style logic but without formal structure.
- ActiveJob: For background jobs that also represent commands.
🛠️ Technical Q&A – Command Pattern in Rails
Q1: What is the Command Pattern in Rails?
A: The Command Pattern wraps a specific business action in a single class (e.g., CreateUserCommand
), allowing it to be reused, tested, queued, or replaced easily.
Q2: What does a command class look like?
A: It usually contains an initialize
method to receive data and a call
method to run the action.
class CreateUserCommand
def initialize(params)
@params = params
end
def call
user = User.new(@params)
raise "Invalid user" unless user.save
user
end
end
Q3: How is it different from a service object?
A: A command is a type of service object, but it always follows a pattern:
- One action
- Callable via
.call
- Focused and isolated
Q4: Where should command classes be stored?
A: Inside app/commands
. You can namespace them by feature (e.g., Admin::SendReportCommand
).
Q5: How do you use a command in a controller?
# In controller
@user = CreateUserCommand.new(user_params).call
Q6: Can a command return values?
A: Yes. It can return the result of the operation (e.g., a user object) or raise an error on failure.
Q7: Can commands be reused in jobs or services?
A: Absolutely. That’s one of their strengths. Example:
class CreateUserJob < ApplicationJob
def perform(params)
CreateUserCommand.new(params).call
end
end
Q8: How do you test a command object?
A: Call the command with test data and check for expected results.
it "creates a user" do
user = CreateUserCommand.new(valid_params).call
expect(user).to be_persisted
end
Q9: What if the command fails?
A: Commands usually raise an exception or return a result object (like with dry-monads
).
Q10: Should commands be idempotent?
A: Yes, ideally. Running the same command twice shouldn't cause data corruption or duplication.
✅ Best Practices with Examples – Command Pattern in Rails
1. ✅ Use one class per action
Keep your command focused on one task. Don’t combine user creation and analytics tracking in the same class.
# Good
class CreateUserCommand; def call; ...; end; end
# Bad
class UserCommand; def create_and_notify_and_log; ...; end; end
2. ✅ Use a call
method as the entry point
This makes the command easier to understand and consistent across your codebase.
def call
# perform the action here
end
3. ✅ Raise exceptions on failure or use dry-monads
for control
This allows you to handle errors explicitly and keeps the behavior predictable.
raise StandardError, "Invalid user" unless user.save
4. ✅ Store all command classes in app/commands
Keep them organized for easy discovery and maintenance.
app/
└── commands/
└── create_user_command.rb
5. ✅ Keep your commands dependency-free (no Rails-specific methods)
This makes them easier to test, reuse in jobs, rake tasks, or even CLI scripts.
6. ✅ Inject dependencies (params or services) via initialize
Use constructor arguments to pass data or service objects into the command.
def initialize(params, notifier: UserMailer)
@params = params
@notifier = notifier
end
7. ✅ Return meaningful data (not just true/false)
Return the created object, or a result object, so the caller can act on it.
user = CreateUserCommand.new(params).call
redirect_to user
8. ✅ Add matching test specs
Test for both success and failure scenarios with real inputs.
expect {
CreateUserCommand.new(invalid_params).call
}.to raise_error(StandardError)
9. ✅ Use commands in controllers, jobs, services, and rake tasks
Don’t repeat logic across your app. Centralize in commands.
# Controller
CreateUserCommand.new(params).call
# Job
SendInvoiceCommand.new(invoice_id).call
10. ✅ Name commands clearly and consistently
Use verbs: CreateUserCommand
, SendReportCommand
, CancelOrderCommand
.
🌍 Real-World Use Cases
- ✅ Creating a user and sending welcome email
- 📦 Processing an e-commerce order
- 📧 Sending bulk marketing emails
- 📊 Exporting a report
- 🔁 Cancelling a subscription
Domain-Driven Design (DDD) in Rails
🧠 Detailed Explanation – Domain-Driven Design (DDD)
Domain-Driven Design (DDD) is an approach to software development that focuses on the real-world business logic behind your application. Instead of organizing your code by type (controllers, models, etc.), DDD encourages you to organize it by business area or domain.
🧠 Why use DDD in Rails?
Rails apps often become messy as they grow because everything goes into models or controllers. DDD helps by splitting logic into **domains** (e.g., Orders, Payments, Users) and grouping related behavior together.
🎯 Core idea:
- 👥 Work with your business team to understand their language and processes
- 🧠 Model that understanding in code
- 📦 Organize your app by “bounded contexts” instead of types (like models/controllers)
📦 Example:
Let’s say your app has payments and inventory. Instead of stuffing everything into models:
# ❌ Bad – everything in one fat model
class Order < ApplicationRecord
def send_invoice
# invoice logic
end
def update_stock
# inventory logic
end
end
Use DDD to split it:
# ✅ Better
# app/domains/billing/invoice_generator.rb
module Billing
class InvoiceGenerator
def initialize(order); @order = order; end
def generate; ... end
end
end
# app/domains/inventory/stock_updater.rb
module Inventory
class StockUpdater
def initialize(order); @order = order; end
def update; ... end
end
end
🗂 Folder Structure Example:
app/
├── domains/
│ ├── billing/
│ │ └── invoice_generator.rb
│ ├── inventory/
│ │ └── stock_updater.rb
│ └── users/
│ └── register_user.rb
🧠 Think of it like:
Instead of organizing by MVC (model, view, controller), you organize by how your business works:
- 📦 Bounded Context = A self-contained area (e.g., Inventory)
- 🎯 Command = One task (e.g., RegisterUser)
- 🔄 Aggregate = A business object and its related data/actions
Bottom line: DDD helps your Rails app grow in a clean, maintainable, and business-aligned way.
📘 Key Terms & Concepts – Domain-Driven Design (DDD)
🔖 Term | 📄 Description |
---|---|
Domain | The business area your software is modeling (e.g., Billing, Inventory, Shipping). |
Bounded Context | A logical boundary around a part of your application. Inside it, models and logic are self-contained and speak one language. |
Entity | An object with a unique identity (e.g., User , Order ). It can change over time. |
Value Object | An immutable object defined only by its data (e.g., Money , Address ). No ID. |
Aggregate | A root entity that groups related objects and enforces business rules. Example: Order contains OrderItems . |
Domain Service | A service that handles domain logic not naturally belonging to a specific entity or value object. |
Application Service | Orchestrates domain services and aggregates to fulfill a use case like "place order". |
Command | A single business action (e.g., CreateInvoiceCommand ) encapsulated as an object. |
Ubiquitous Language | The shared language between developers and domain experts. Used in code, conversations, and docs. |
Anti-Corruption Layer (ACL) | A boundary that protects your domain from leaking external or legacy system models directly into it. |
Module/Namespace | A Ruby construct used to group logic under a bounded context (e.g., module Inventory ). |
🔁 Flow – How to Use Domain-Driven Design in Rails
- 1. Understand the Business Domain
Work closely with domain experts (e.g., product owners, clients) to understand how their world works. Write down their terminology. - 2. Define Bounded Contexts
Break your system into clear, separate business areas — e.g.,Payments
,Inventory
,Users
. - 3. Create Folders by Domain, Not Type
Insideapp/domains
, create folders for each bounded context:app/domains/ ├── billing/ ├── inventory/ ├── users/
- 4. Move Business Logic to Domain Classes
Use plain Ruby objects to handle logic like:Billing::InvoiceGenerator
Inventory::StockAdjuster
- 5. Use Commands for Actions
For each task, create a command object likeCreateOrderCommand
orShipItemCommand
. These live inside the domain and execute logic. - 6. Keep Domain Objects Framework-Free
Don’t include ActiveRecord, helpers, or Rails concerns. These are pure business logic. - 7. Coordinate with Application Services
In Rails controllers, call application services or command objects to execute domain logic.
Example:
# Controller
def create
@invoice = Billing::CreateInvoiceCommand.new(order_id: params[:id]).call
redirect_to @invoice
end
📌 Where & How to Use DDD in Rails
📍 Area | 💡 How DDD Helps |
---|---|
🛒 E-Commerce (Checkout, Inventory, Payments) | Keeps pricing, tax, shipping, and stock logic isolated and reusable. |
🏥 Healthcare Systems | Separates patient data, appointments, billing, and prescriptions into clean contexts. |
📦 Logistics & Shipping | Divides routes, package tracking, and delivery workflows into separate modules. |
📊 Report Generation | Keeps data aggregation logic in a separate domain layer. |
💼 SaaS Applications | Isolates user billing, access control, and analytics into distinct services. |
✅ Rule of Thumb: Use DDD when your business logic is growing and starting to feel messy in controllers or models.
🧩 Gems & Libraries – Domain-Driven Design (DDD) in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
dry-struct | Immutable value objects. Helps define domain models with strict types. | Creating value objects like Money , Address . |
dry-types | Type system for Ruby. Pairs with dry-struct for defining types. | Add strong typing to your domain logic. |
dry-monads | Provides Success /Failure objects for predictable flows. | Return explicit outcomes from domain commands/services. |
interactor | Encapsulates business logic into Interactors . Provides context and failure support. | Modeling commands like CreateOrder or SendInvoice . |
trailblazer-operation | Full-fledged DDD-aligned operation pattern with validations, contracts, and workflows. | Complex use case orchestration in a structured DDD approach. |
zeitwerk | Rails autoloader. Supports namespaced modules used in DDD folder structures. | Auto-loading namespaced domains like Billing::InvoiceGenerator . |
dry-container | Dependency injection container. Keeps services/commands pluggable and isolated. | Register and resolve domain services cleanly in contexts. |
command_kit | For building CLI-style commands. Not Rails-specific but helpful in DDD-based infrastructure. | Creating CLI tools that trigger domain actions. |
🛠️ Best Implementation – Domain-Driven Design in Rails
This example shows how to implement DDD in a real-world Rails application for a Billing domain, which includes generating invoices and sending payment emails.
📁 Step 1: Create the Domain Folder Structure
Group domain logic by context under app/domains/
.
app/
├── domains/
│ ├── billing/
│ │ ├── create_invoice_command.rb
│ │ ├── invoice.rb
│ │ └── mailers/
│ │ └── invoice_mailer.rb
│ └── inventory/
│ └── stock_adjuster.rb
📦 Step 2: Create a Value Object (optional but recommended)
Use `dry-struct` for immutable, typed data objects.
# app/domains/billing/value_objects/money.rb
module Billing
module ValueObjects
class Money < Dry::Struct
attribute :amount, Types::Coercible::Float
attribute :currency, Types::Strict::String
end
end
end
⚙️ Step 3: Write a Domain Command (Business Action)
# app/domains/billing/create_invoice_command.rb
module Billing
class CreateInvoiceCommand
def initialize(order)
@order = order
end
def call
total = @order.line_items.sum(&:price)
invoice = Invoice.create!(order_id: @order.id, total: total)
InvoiceMailer.with(invoice: invoice).deliver_later
invoice
end
end
end
📨 Step 4: Domain Mailer (Optional Email Layer)
# app/domains/billing/mailers/invoice_mailer.rb
module Billing
class InvoiceMailer < ApplicationMailer
def with(invoice:)
@invoice = invoice
mail(to: @invoice.customer_email, subject: "Your Invoice")
end
end
end
🧼 Step 5: Use Command in Controller or Service
# app/controllers/orders_controller.rb
def create_invoice
invoice = Billing::CreateInvoiceCommand.new(@order).call
redirect_to invoice_path(invoice), notice: "Invoice created"
rescue => e
redirect_to @order, alert: e.message
end
🧪 Step 6: Add a Unit Test
# spec/domains/billing/create_invoice_command_spec.rb
require 'rails_helper'
RSpec.describe Billing::CreateInvoiceCommand do
let(:order) { create(:order_with_items) }
it "creates an invoice and sends email" do
expect {
Billing::CreateInvoiceCommand.new(order).call
}.to change(Invoice, :count).by(1)
.and change { ActionMailer::Base.deliveries.count }.by(1)
end
end
📌 Bonus: Use `zeitwerk` for Autoloading
Rails 6+ autoloads namespaced modules like Billing::CreateInvoiceCommand
by default — no need to require manually.
✅ Summary
- Group logic by domain (not by Rails type)
- Use commands to encapsulate actions
- Use value objects for clean, reusable data types
- Keep domain logic free of controller/model clutter
- Make everything testable in isolation
💡 Examples
app/domains/billing/invoice_aggregator.rb
module Billing
class InvoiceAggregator
def initialize(order)
@order = order
end
def generate
# Invoice logic across order items
end
end
end
Controller:
Billing::InvoiceAggregator.new(order).generate
🔁 Alternative Concepts
- Service Layer: Central place for domain logic without domain-based boundaries
- Modular Monolith: Uses DDD-like separation without microservices
- Active Record pattern: Traditional Rails approach—great for CRUD but limited for complexity
🛠️ Technical Q&A – Domain-Driven Design (DDD) in Rails
Q1: What is Domain-Driven Design?
A: DDD is a software design approach that structures code based on the business domain. Instead of organizing by MVC types (models/controllers), DDD uses bounded contexts, entities, value objects, and aggregates to reflect real-world behavior and rules in code.
Q2: What is a Bounded Context in DDD?
A: A bounded context defines a logical boundary within the application where a specific domain model applies. Each context has its own language, rules, and logic. In Rails, you can implement this with modules and folders like app/domains/billing
.
Q3: How is an Entity different from a Value Object?
A: An Entity has a unique identifier (like a User
or Order
) and can change over time. A Value Object (like Money
or Address
) has no identity and is immutable — it's defined only by its values.
# Value Object using dry-struct
module Billing
class Money < Dry::Struct
attribute :amount, Types::Coercible::Float
attribute :currency, Types::String
end
end
Q4: What is an Aggregate?
A: An aggregate is a cluster of domain objects (entities + value objects) treated as a single unit. The root (aggregate root) controls access to its parts.
# Example: Order as aggregate root
class Order < ApplicationRecord
has_many :order_items
def add_item(product, quantity)
order_items.build(product: product, quantity: quantity)
end
end
Q5: How do you structure DDD folders in Rails?
A: Under app/domains
, create folders for each context:
app/domains/
├── billing/
│ ├── create_invoice_command.rb
│ ├── value_objects/
│ └── invoice_mailer.rb
├── users/
└── inventory/
Q6: How do you keep domain logic out of controllers?
A: Use command objects or application services inside your domain folder:
# Controller
def create
Billing::CreateInvoiceCommand.new(order).call
end
Q7: How do you test domain logic in DDD?
A: Write unit tests for each command or service, mocking only external boundaries.
RSpec.describe Billing::CreateInvoiceCommand do
it "creates an invoice and sends mail" do
expect {
described_class.new(order).call
}.to change(Invoice, :count).by(1)
end
end
Q8: What is Ubiquitous Language?
A: A shared vocabulary between developers and domain experts. This language is used in both code and conversations to avoid misunderstandings.
Q9: What is the role of Application Services?
A: Application services coordinate domain objects to fulfill use cases. They do not contain business logic themselves.
Q10: When should you apply DDD in a Rails app?
A: DDD is helpful when:
- Business logic is complex or growing fast
- Multiple teams or subdomains are involved
- Your app needs long-term maintainability
✅ Best Practices with Examples – Domain-Driven Design in Rails
1. ✅ Organize by Domain, Not by Type
Instead of placing all models or services in one folder, group by business context.
# Good
app/domains/billing/invoice_generator.rb
# Bad
app/models/invoice.rb
app/services/generate_invoice.rb
2. ✅ Keep Domain Logic Framework-Free
Domain classes should not depend on Rails features like ActiveRecord or helpers.
# Good
module Billing
class TaxCalculator
def initialize(amount); @amount = amount; end
def call; @amount * 0.05; end
end
end
3. ✅ Use Value Objects for Repeated Concepts
Instead of passing raw hashes or primitives, use value objects for things like money or address.
Billing::Money.new(amount: 99.99, currency: "USD")
4. ✅ One Command = One Action
Each command object should encapsulate one clear business action.
# app/domains/users/register_user_command.rb
class RegisterUserCommand
def initialize(params); @params = params; end
def call; User.create!(@params); end
end
5. ✅ Keep Controllers Thin
Use domain commands or services to handle business logic.
# app/controllers/users_controller.rb
def create
@user = Users::RegisterUserCommand.new(user_params).call
redirect_to @user
end
6. ✅ Isolate Bounded Contexts
Never share models or services across contexts like Inventory
and Billing
.
7. ✅ Use Explicit Return Types (Success/Failure)
Use dry-monads
or similar to return structured results instead of booleans or nil.
result = Billing::CreateInvoiceCommand.new(order).call
if result.success?
redirect_to result.value!
else
flash[:alert] = result.failure
end
8. ✅ Test Domains in Isolation
Write RSpec unit tests for domain classes, not for Rails controllers or helpers.
RSpec.describe Billing::TaxCalculator do
it "calculates 5% tax" do
expect(described_class.new(100).call).to eq(5.0)
end
end
9. ✅ Avoid Fat Models, Use Aggregates Instead
Model relationships inside aggregates and use methods like add_item
or calculate_total
.
10. ✅ Reflect Ubiquitous Language in Code
Use domain terms your business uses. If your business says “fulfill order”, don’t name the method process_cart
.
🌍 Real-World Use Cases
- 🛒 E-commerce apps separating Checkout, Inventory, and Shipping
- 🏥 Healthcare systems isolating Patient, Diagnosis, and Billing contexts
- 🏦 Banking apps with Loans, Accounts, Payments as separate domains
Fat Model, Skinny Controller in Rails
🧠 Detailed Explanation – Fat Model, Skinny Controller
Fat Model, Skinny Controller is a common Rails practice that helps keep your code clean, organized, and easy to manage.
🧾 What does it mean?
It means putting most of your business logic (the rules and behavior of your app) inside your Model
files, instead of in your Controller
files.
Controllers should only:
- Accept requests from the browser (like "create user" or "place order")
- Call the model to do the work
- Return a response (like redirect or render a page)
Models should handle all the actual work, like:
- Validating input
- Saving data
- Sending emails
- Calculating totals
- Running complex logic
📦 Example – Before and After
❌ Bad: Fat Controller
# users_controller.rb
def create
@user = User.new(params[:user])
if @user.save
UserMailer.welcome(@user).deliver_later
Analytics.track_signup(@user)
redirect_to @user
else
render :new
end
end
This controller is doing too much: saving the user, sending mail, tracking analytics.
✅ Good: Fat Model
# users_controller.rb
def create
@user = User.register(params[:user])
if @user.persisted?
redirect_to @user
else
render :new
end
end
# user.rb
def self.register(attrs)
user = new(attrs)
if user.save
UserMailer.welcome(user).deliver_later
Analytics.track_signup(user)
end
user
end
This is better! The controller is simple, and the model takes care of the business logic.
🎯 Why do we use it?
- ✅ Makes controllers easier to read
- ✅ Keeps logic in one place (the model)
- ✅ Makes it easier to test and reuse your logic
- ✅ Follows the principle: “Skinny Controller, Fat Model”
🤔 What if my model gets too fat?
That’s okay! You can move complex logic into:
- Service objects (e.g.,
UserSignupService
) - Form objects (e.g.,
RegistrationForm
) - Command classes (e.g.,
CreateUserCommand
)
This way, your logic is still out of the controller and easy to manage.
📌 Summary
- Controllers = Thin, only handle input/output
- Models = Handle core logic and rules
- If logic gets big, move it to plain Ruby objects (services, commands)
📘 Key Terms & Concepts – Fat Model, Skinny Controller
🔖 Term | 📄 Description |
---|---|
Fat Model | A Rails model that contains most of the business logic (validations, callbacks, helpers) for a specific resource. |
Skinny Controller | A Rails controller that only handles request flow (e.g., receive data, redirect/render) and delegates all business logic to models or services. |
Business Logic | Rules, calculations, and decisions specific to your app's domain (e.g., how to register a user or calculate order totals). |
Service Object | A plain Ruby object that encapsulates complex business logic outside of models/controllers. |
Model Method | A custom method inside a model to perform logic such as `User#send_welcome_email`. |
Callback | A hook in a model (like `before_save`) used to run logic during the ActiveRecord lifecycle. Best used carefully to avoid hidden logic. |
Form Object | An object used to handle multiple model inputs in a single form, keeping models and controllers clean. |
Command Object | Encapsulates one business action into a single object. Example: `RegisterUserCommand`. |
Single Responsibility Principle (SRP) | A programming principle stating that each class/module should only do one thing. Helps keep models and controllers clean and focused. |
Plain Old Ruby Object (PORO) | A regular Ruby class that doesn't inherit from Rails classes (like ActiveRecord). Great for reusable logic. |
🔁 Flow – How Fat Model, Skinny Controller Works
- 1. Request Comes In:
A user submits a form or clicks a link, and Rails routes the request to a controller action. - 2. Controller Handles Request:
The controller receives input (like params) and delegates the real work to a model method or service object. - 3. Model Executes Business Logic:
The model validates data, performs calculations, sends emails, saves records, or interacts with other models. - 4. Controller Responds:
Once the model finishes, the controller renders a view or redirects to another page. - 5. Result Is Shown:
The user sees the result (success message, new page, or error).
✅ The main rule: Controller = Light Logic + Routing
and Model = All Core Logic
📌 Where & How to Use This Pattern
📍 Area in App | 💡 How Fat Model Helps |
---|---|
🧾 User Registration | Put email sending, validation, profile setup inside model or service object. |
🛒 E-Commerce Checkout | Controller just passes cart ID – pricing, tax, and stock logic goes in model. |
📧 Contact Form / Support Tickets | Model handles sending emails, saving messages, notifications – controller stays clean. |
📦 Order Fulfillment | Controller calls a method like `Order#fulfill!` that runs shipping, billing, stock updates. |
💳 Subscriptions / Payments | Models handle gateway API calls (e.g., Stripe), retries, and errors; controller only triggers them. |
👥 User Profiles | Controller calls `User#update_profile` – model handles avatar processing, validation, and auditing. |
📊 Reports & Stats | Data aggregation and formatting should live in the model or a query object. |
📌 Pro Tip: Use this pattern for 90% of your Rails actions unless the logic is so complex that it needs its own object or module.
🧩 Gems & Libraries – Fat Model, Skinny Controller in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
interactor | Encapsulates business logic into “interactors” with context and failure handling. | Replacing long controller logic with reusable actions (e.g., `CreateUser`). |
dry-monads | Functional constructs like `Success`, `Failure`, and `Maybe` for cleaner service logic. | Returning structured results from service/model logic. |
dry-struct | Immutable value objects with strict type validation. | Replacing complex hashes in models with structured objects. |
reform | Form object gem that combines multiple models or validations into a single class. | Keeping forms clean (e.g., user + address form). |
trailblazer | Rails architecture framework that enforces service objects, form objects, and operations. | Building structured, scalable Rails apps from the start. |
active_interaction | Another gem for writing business logic as "interactions" with built-in validation. | Lightweight alternative to `interactor` with validation. |
service_pattern | Minimalistic gem for creating service classes with a `.call` method. | Good for introducing service objects into legacy apps. |
delegate (built-in Ruby) | Lets one object forward method calls to another, useful for delegating logic. | Keeping your models clean by delegating logic to plain Ruby objects. |
🛠️ Best Implementation – Fat Model, Skinny Controller in Rails
This example shows how to apply the Fat Model, Skinny Controller pattern by moving business logic out of the controller and into the model (or a service object if the logic is complex).
📁 Step 1: Problem Example – Fat Controller
This is how many developers start: putting everything in the controller, which becomes hard to manage and test.
# app/controllers/users_controller.rb
def create
@user = User.new(user_params)
if @user.save
UserMailer.welcome(@user).deliver_later
AnalyticsService.track_signup(@user)
flash[:notice] = "User created successfully!"
redirect_to @user
else
render :new
end
end
👎 This controller does too much: validation, saving, mailing, tracking, and UI handling.
✅ Step 2: Move Logic to the Model
Move all the logic into a class method in the model (e.g., User.register
).
# app/models/user.rb
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
validates :password, presence: true
def self.register(params)
user = new(params)
if user.save
UserMailer.welcome(user).deliver_later
AnalyticsService.track_signup(user)
end
user
end
end
✅ Step 3: Make the Controller Clean
Now the controller is focused only on request/response, not business logic.
# app/controllers/users_controller.rb
def create
@user = User.register(user_params)
if @user.persisted?
redirect_to @user, notice: "User created!"
else
render :new
end
end
👍 This controller is readable, testable, and follows the SRP (Single Responsibility Principle).
🔁 Step 4: What If Logic Is Too Big?
If the logic grows too large, extract it into a Service Object instead of stuffing it in the model.
# app/services/user_registration_service.rb
class UserRegistrationService
def initialize(params)
@params = params
end
def call
user = User.new(@params)
if user.save
UserMailer.welcome(user).deliver_later
AnalyticsService.track_signup(user)
end
user
end
end
# app/controllers/users_controller.rb
def create
@user = UserRegistrationService.new(user_params).call
if @user.persisted?
redirect_to @user
else
render :new
end
end
🧪 Step 5: Add Tests
# spec/services/user_registration_service_spec.rb
RSpec.describe UserRegistrationService do
let(:params) { { email: "test@example.com", password: "secret" } }
it "creates a user and sends email" do
expect {
service = UserRegistrationService.new(params)
user = service.call
}.to change(User, :count).by(1)
.and change { ActionMailer::Base.deliveries.count }.by(1)
end
end
📌 Summary of Best Practices
- ✅ Keep controller actions short (ideally under 10 lines)
- ✅ Move all logic into models or service objects
- ✅ Use value objects or command classes when logic grows
- ✅ Write unit tests for the model/service, not the controller
💡 Example
# ❌ Bad – Logic inside Controller
def create
@user = User.new(params[:user])
if @user.save
UserMailer.welcome(@user).deliver_later
redirect_to @user
else
render :new
end
end
# ✅ Good – Logic moved to Model or Service
def create
@user = User.register(params[:user])
if @user.persisted?
redirect_to @user
else
render :new
end
end
# In Model:
def self.register(attrs)
user = new(attrs)
if user.save
UserMailer.welcome(user).deliver_later
end
user
end
🔁 Alternative Concepts
- Service Objects: Move complex logic from models to `app/services`
- Interactor Pattern: Use `interactor` gem to encapsulate operations
🛠️ Technical Q&A – Fat Model, Skinny Controller in Rails
Q1: What is the Fat Model, Skinny Controller pattern?
A: It’s a Rails architectural practice where controllers only handle routing and responses, while models (or service objects) contain the application's business logic. This keeps controllers simple and models powerful.
Q2: Why is this pattern preferred in Rails?
A: Because it follows the Single Responsibility Principle — each class or file should do one job. Controllers should handle HTTP requests; models handle logic like validations, data changes, or notifications.
Q3: How do I move logic from controller to model?
A: Create a method in your model (class or instance method), and move logic into it. Example:
# Controller
@user = User.register(user_params)
# Model
def self.register(params)
user = new(params)
user.save
UserMailer.welcome(user).deliver_later
user
end
Q4: What if my model becomes too large?
A: Use Service Objects or Command Objects to extract logic from the model. Example:
# app/services/user_signup_service.rb
class UserSignupService
def initialize(params); @params = params; end
def call
user = User.new(@params)
user.save
Analytics.track_signup(user)
user
end
end
Q5: Can we use this pattern with callbacks?
A: You can, but use callbacks carefully. Avoid hiding logic in callbacks unless it’s tightly related to saving records (like timestamps or normalizations).
# Good use of callback
before_save :normalize_email
def normalize_email
self.email = email.downcase
end
Q6: How do I test logic moved to the model?
A: You write **unit tests** for model methods or service objects instead of controller tests. Example:
RSpec.describe User, type: :model do
it "sends welcome email when registering" do
expect {
User.register({email: "test@example.com", password: "secret"})
}.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
Q7: What’s a common beginner mistake with this pattern?
A: Putting logic back into controllers during refactoring, like handling side effects or errors there. Keep logic in the model/service, and just handle success/failure in the controller.
Q8: When should I choose a service object over a fat model?
A: When logic touches multiple models or becomes long (e.g., multi-step processes), extract it into a service. Rule of thumb: if a method is >15–20 lines or uses multiple `if`/`else` paths, move it.
Q9: What gem can help structure this pattern better?
A: Use interactor
or service_pattern
gems to structure business logic cleanly outside models and controllers.
# Interactor example
class RegisterUser
include Interactor
def call
user = User.new(context.params)
user.save!
UserMailer.welcome(user).deliver_later
context.user = user
end
end
Q10: Does this pattern apply only to models?
A: No. If your business logic doesn't belong to one model, use POROs (Plain Old Ruby Objects), Services, or Interactors. The idea is to **separate concerns** — not to make the model a dumping ground.
✅ Best Practices with Examples – Fat Model, Skinny Controller in Rails
1. ✅ Keep Controllers Thin and Focused
Controllers should only receive input, delegate work, and return a response.
# Good
def create
@user = User.register(user_params)
redirect_to @user
end
2. ✅ Move Complex Logic into the Model
Business rules, email sending, and calculations should live in the model or a service.
# user.rb
def self.register(params)
user = new(params)
if user.save
UserMailer.welcome(user).deliver_later
end
user
end
3. ✅ Use Service Objects for Multi-Model Logic
If logic involves more than one model or external API, create a class in app/services/
.
# app/services/create_order.rb
class CreateOrder
def initialize(user, cart)
@user = user
@cart = cart
end
def call
order = Order.create!(user: @user)
@cart.items.each do |item|
order.line_items.create!(product: item.product, quantity: item.quantity)
end
order
end
end
4. ✅ Reuse Business Logic with Model Methods
Make common actions callable like user.send_welcome_email
.
# user.rb
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
5. ✅ Use Plain Ruby Objects (POROs) When Needed
Not everything needs to be a model. Use regular Ruby classes when logic doesn’t belong to ActiveRecord.
# app/lib/tax_calculator.rb
class TaxCalculator
def initialize(amount); @amount = amount; end
def call; @amount * 0.05; end
end
6. ✅ Avoid Hidden Logic in Callbacks
Callbacks like after_save
can make code hard to debug. Prefer explicit method calls in your model or service.
# ❌ Hidden in callback
after_save :send_invoice
# ✅ Better
def complete_order
save!
InvoiceMailer.send_invoice(self).deliver_later
end
7. ✅ Add Unit Tests for Models and Services
Test your logic outside the controller. Controller tests should be minimal.
# spec/services/create_order_spec.rb
RSpec.describe CreateOrder do
it "creates an order with items" do
order = CreateOrder.new(user, cart).call
expect(order.line_items.count).to eq(cart.items.count)
end
end
8. ✅ Name Service Classes with a Verb
Use clear names like CreateInvoice
, RegisterUser
, SyncPayments
.
# app/services/sync_payments.rb
class SyncPayments
def initialize(account); @account = account; end
def call; ... end
end
9. ✅ Group Files by Responsibility
Use folders like app/services
, app/lib
, or app/forms
to separate logic clearly.
app/
├── controllers/
├── models/
├── services/
│ └── register_user.rb
├── mailers/
├── forms/
├── lib/
10. ✅ Keep Code Intentional and Readable
Your code should clearly express intent. The controller should say “register user”, not “create user and send mail and do analytics”.
# Good
User.register(params)
🌍 Real-World Use Cases
- 📩 Signup flows where email confirmation, billing, and profile setup are handled in models/services
- 🛒 Checkout flow – price calculation, stock updates go into model logic
Hexagonal Architecture (Ports and Adapters) in Rails
🧠 Detailed Explanation – Hexagonal Architecture (Ports & Adapters)
Hexagonal Architecture, also called Ports and Adapters, is a way of organizing your code so that your core application logic is protected from outside systems like databases, web controllers, APIs, or third-party libraries.
Imagine your app is a **hexagon (or a circle)** in the center, surrounded by the outside world. You talk to the outside world through **ports** (interfaces), and the outside world connects to you through **adapters** (implementations).
🔌 What are Ports?
Ports are just interfaces. They define how the core logic expects to talk to things like databases, emails, or APIs. The core doesn’t care how these things are actually implemented.
# port: order_repo.rb (interface)
class OrderRepo
def save(order)
raise NotImplementedError
end
end
🧩 What are Adapters?
Adapters are real implementations of those interfaces. For example, one adapter might use ActiveRecord to save data; another might use a JSON API.
# adapter: order_repository.rb (implements the port)
class OrderRepository < OrderRepo
def save(order)
OrderRecord.create!(order.attributes)
end
end
🏗️ Core Logic Example
The main logic lives in a service object or use case. It depends on ports (not Rails).
# core/use_cases/create_order.rb
class CreateOrder
def initialize(order_repo, notifier)
@order_repo = order_repo
@notifier = notifier
end
def call(data)
order = Order.new(data)
@order_repo.save(order)
@notifier.send("Order created successfully!")
end
end
Notice: The core doesn’t care about database, email, or Rails. It just does its job.
🚪 Why use Hexagonal Architecture?
- ✅ You can test business logic easily (mock ports)
- ✅ You can change frameworks or tools without breaking the core
- ✅ You keep Rails details out of your logic
- ✅ Your app becomes easier to understand and extend
📌 Analogy
Think of your app like a power plug:
- 🔌 Port: The plug socket (interface)
- ⚙️ Adapter: The plug that fits into the socket
- 🔋 Core: Your actual device that runs regardless of how it’s powered
🔁 Summary
- ✅ Core logic depends on ports (interfaces)
- ✅ Outside systems use adapters to implement those ports
- ✅ Makes your code clean, testable, and flexible
- ✅ Great for large or long-term Rails apps
📘 Key Terms & Concepts – Hexagonal Architecture (Ports & Adapters)
🔖 Term | 📄 Description |
---|---|
Hexagonal Architecture | A software design pattern that separates the core application logic from external dependencies like databases, web UIs, or APIs. |
Port | An interface that defines how the application interacts with the outside world. It's a contract that external tools must follow. |
Adapter | A class or module that implements a port. It connects the app to the real world, such as databases, APIs, or UI controllers. |
Core | The center of the architecture. It contains all the business logic and domain models, and is independent of frameworks or tools. |
Use Case | A specific business action (e.g., `CreateOrder`, `RegisterUser`) implemented inside the core and triggered through a port. |
Dependency Inversion | A principle that allows the core to depend on interfaces (ports) instead of concrete classes (adapters). This helps decouple the system. |
Inbound Adapter | An adapter that brings data into the app, like a Rails controller or GraphQL resolver calling a use case. |
Outbound Adapter | An adapter that the app uses to talk to external systems, like a repository or mailer. |
Boundary | The dividing line between the core application and the outside world. Ports define this boundary. |
Testability | Hexagonal architecture increases testability by allowing you to mock ports and test core logic in isolation. |
Framework Independence | The core logic does not depend on Rails, ActiveRecord, or any gem — making it portable and reusable. |
🔁 Flow – How Hexagonal Architecture Works
- 1. A Request Comes In:
A user sends a request (e.g., via web, API, or CLI) that is handled by an inbound adapter such as a Rails controller. - 2. The Adapter Calls a Port (Interface):
The adapter calls a port (interface), which connects to the core business logic — like a use case class. - 3. The Core Logic Executes:
The core logic (use case) performs business operations, using other ports like `Repository` or `Notifier` if needed. - 4. The Core Calls Outbound Adapters:
The core logic sends data to an outbound adapter — like a database adapter or an email sender. - 5. The Result Is Returned:
The controller (inbound adapter) receives the result and responds to the user (e.g., render view, JSON, or redirect).
📌 Key Idea: The core logic never knows if it’s being called by a web app, CLI, or test — it only talks to interfaces (ports).
📌 Where & How to Use Hexagonal Architecture in Rails Apps
📍 App Area | 💡 Why Hexagonal Architecture Helps |
---|---|
🛒 Order Checkout Flow | Keeps logic like inventory checks, discounts, and email notifications isolated from controllers and database logic. |
💬 Messaging / Notifications | Switch between SMS, email, and push notifications without changing the core logic. |
💼 Payment Processing | Use different payment gateways (e.g., Stripe, PayPal) via adapters while core logic stays the same. |
📈 Reporting & Analytics | Track events with different providers (Mixpanel, Segment, or internal) by plugging in adapters. |
🔐 Authentication | Use different login systems (Devise, OAuth, external SSO) by connecting via ports. |
🧪 Test Environments | Swap real adapters with mocks or stubs to test core business logic without external dependencies. |
✅ Rule of Thumb: Use Hexagonal Architecture when your logic grows beyond simple CRUD, or when you want testability, flexibility, and long-term maintainability.
🧩 Gems & Libraries – Hexagonal Architecture in Rails
🔧 Gem / Tool | 📄 Description | ✅ Use Case |
---|---|---|
dry-rb (dry-struct, dry-types) | Immutable objects and type-safe data structures. | Creating domain entities in your core that are clean and safe to pass around. |
dry-container | Simple dependency injection container for registering and resolving adapters. | Managing which adapter to use for each port. |
dry-auto_inject | Adds auto-injection support using dry-container. | Injecting repositories, notifiers, or other ports into use cases. |
zeitwerk | Rails' autoloader that supports namespaced directories and modules. | Auto-loading structured layers like app/core , app/adapters . |
Interactor | Encapsulates a single business use case. | Writing clean, reusable use case classes (e.g., `CreateUser`, `SendOrder`). |
service_pattern | Minimal gem for defining service objects with `.call` convention. | Simple alternative for use cases without external dependencies. |
dry-monads | Returns clean `Success` or `Failure` results instead of booleans or exceptions. | Handling control flow in core logic with fewer bugs and more clarity. |
rspec-mocks / factory_bot / faker | Testing tools to mock adapters and isolate core logic. | Unit testing core logic by mocking ports (not the database or APIs). |
🛠️ Best Implementation – Hexagonal Architecture in Rails
This example shows how to implement Hexagonal Architecture by cleanly separating core logic from external systems like databases and notifications using ports and adapters.
📁 Step 1: Folder Structure
app/
├── core/
│ └── use_cases/
│ └── create_order.rb
├── domains/
│ └── order.rb
├── ports/
│ ├── order_repository.rb
│ └── notifier.rb
├── adapters/
│ ├── repositories/
│ │ └── active_record_order_repository.rb
│ └── notifiers/
│ └── email_notifier.rb
├── controllers/
│ └── orders_controller.rb
Each layer is separated by responsibility: core logic, domain model, ports (interfaces), and adapters (implementations).
📦 Step 2: Define a Domain Model (Entity)
# app/domains/order.rb
class Order
attr_reader :id, :user_id, :items, :total
def initialize(user_id:, items:)
@user_id = user_id
@items = items
@total = calculate_total
end
private
def calculate_total
items.sum { |item| item[:price] * item[:quantity] }
end
end
🛣️ Step 3: Define Ports (Interfaces)
# app/ports/order_repository.rb
class OrderRepository
def save(order)
raise NotImplementedError
end
end
# app/ports/notifier.rb
class Notifier
def send(message)
raise NotImplementedError
end
end
⚙️ Step 4: Core Use Case (Business Logic)
# app/core/use_cases/create_order.rb
class CreateOrder
def initialize(order_repository:, notifier:)
@order_repository = order_repository
@notifier = notifier
end
def call(user_id:, items:)
order = Order.new(user_id: user_id, items: items)
@order_repository.save(order)
@notifier.send("Order created for user #{user_id}")
order
end
end
🔌 Step 5: Implement Adapters
# app/adapters/repositories/active_record_order_repository.rb
class ActiveRecordOrderRepository < OrderRepository
def save(order)
OrderRecord.create!(
user_id: order.user_id,
total: order.total,
items_data: order.items.to_json
)
end
end
# app/adapters/notifiers/email_notifier.rb
class EmailNotifier < Notifier
def send(message)
NotificationMailer.system_alert(message).deliver_later
end
end
🧠 Step 6: Wire It Up in Controller
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
repository = ActiveRecordOrderRepository.new
notifier = EmailNotifier.new
use_case = CreateOrder.new(order_repository: repository, notifier: notifier)
order = use_case.call(user_id: params[:user_id], items: params[:items])
render json: { status: "success", order_total: order.total }
end
end
🧪 Step 7: Test the Core Logic
# spec/use_cases/create_order_spec.rb
RSpec.describe CreateOrder do
let(:fake_repo) { instance_double("OrderRepository", save: true) }
let(:fake_notifier) { instance_double("Notifier", send: true) }
it "creates an order and sends a notification" do
use_case = CreateOrder.new(order_repository: fake_repo, notifier: fake_notifier)
order = use_case.call(user_id: 1, items: [{ price: 10, quantity: 2 }])
expect(order.total).to eq(20)
expect(fake_repo).to have_received(:save).with(order)
expect(fake_notifier).to have_received(:send).with("Order created for user 1")
end
end
✅ Summary
- 💡 Core logic lives in
app/core/use_cases
- 🔌 External dependencies use
ports
andadapters
- 🧪 Tests mock ports to test logic in isolation
- 🔁 Adapters can be swapped without touching the core logic
- 🚫 No ActiveRecord or Rails dependency inside business logic
💡 Example
# Core Use Case
# app/core/use_cases/create_order.rb
class CreateOrder
def initialize(order_repo, notifier)
@order_repo = order_repo
@notifier = notifier
end
def call(data)
order = Order.new(data)
@order_repo.save(order)
@notifier.send("Order created")
end
end
# Adapter: Repository
# app/adapters/repositories/order_repository.rb
class OrderRepository
def save(order)
OrderRecord.create!(order.attributes)
end
end
# Adapter: Notifier
# app/adapters/notifiers/email_notifier.rb
class EmailNotifier
def send(message)
EmailService.deliver(message)
end
end
🔁 Alternative Concepts
- Service Layer: Simpler but often mixes logic and I/O
- Onion Architecture: Very similar, but layers are enforced inward
- Clean Architecture: Modern evolution with interactor/input/output terminology
🛠️ Technical Q&A – Hexagonal Architecture in Rails
Q1: What is Hexagonal Architecture?
A: It’s an architectural pattern that separates your core application logic from the external world (UI, database, APIs). The app communicates through "Ports" (interfaces) and "Adapters" (implementations).
Q2: What are Ports and Adapters?
A: A Port is an interface that the core logic depends on. An Adapter is a class that implements that interface to connect to external systems.
# Port
class Notifier
def send(message)
raise NotImplementedError
end
end
# Adapter
class EmailNotifier < Notifier
def send(message)
Mailer.deliver(message)
end
end
Q3: Why is Hexagonal Architecture useful in Rails apps?
A: It improves testability, flexibility, and separation of concerns. Your core logic becomes independent of Rails (e.g., ActiveRecord, ActionMailer).
Q4: How do you test core logic in Hexagonal Architecture?
A: You test the core use cases by mocking the adapters (ports), without touching the database or Rails internals.
# spec/use_cases/create_order_spec.rb
let(:repo) { double("OrderRepository", save: true) }
let(:notifier) { double("Notifier", send: true) }
it "creates an order and notifies" do
use_case = CreateOrder.new(order_repository: repo, notifier: notifier)
use_case.call(user_id: 1, items: [{ price: 10, quantity: 2 }])
expect(repo).to have_received(:save)
expect(notifier).to have_received(:send)
end
Q5: Can Hexagonal Architecture be overkill for small apps?
A: Yes. For small CRUD apps, the overhead may not be worth it. But it shines in apps with growing complexity or multiple external integrations.
Q6: What’s the difference between Inbound and Outbound Adapters?
A:
- Inbound Adapters: Bring data into the system (e.g., web controllers, API endpoints)
- Outbound Adapters: Used by the system to reach external services (e.g., database repo, mailer)
Q7: How do you inject dependencies (adapters) into use cases?
A: Pass them through the constructor or use dependency injection tools like dry-container
.
# Manual DI
repo = ActiveRecordOrderRepository.new
notifier = EmailNotifier.new
use_case = CreateOrder.new(order_repository: repo, notifier: notifier)
Q8: How does Hexagonal Architecture improve testing?
A: Core logic becomes testable in isolation, without needing to hit the database or APIs. This leads to faster and more reliable tests.
Q9: Can I still use ActiveRecord and ActionMailer?
A: Yes, but only inside the adapter layer. The core logic should never directly access them.
Q10: How do I structure Hexagonal Architecture in Rails?
A: Follow this common directory pattern:
app/
├── core/ # Use cases
├── domains/ # Domain models (entities)
├── ports/ # Interfaces
├── adapters/ # Implementations (DB, mail, APIs)
├── controllers/ # Inbound Adapters (e.g., HTTP)
✅ Best Practices with Examples – Hexagonal Architecture in Rails
1. ✅ Keep Core Logic Free from Rails
Your use case classes (core logic) should not depend on ActiveRecord, ActionMailer, or Rails concerns.
# Good
def call(user_id:, items:)
order = Order.new(user_id: user_id, items: items)
@repo.save(order)
end
# Bad
OrderRecord.create(user_id: ..., total: ...)
2. ✅ Define Clear Interfaces (Ports)
Use Ruby interfaces (abstract classes or modules) to define what your adapters must implement.
# port/notifier.rb
class Notifier
def send(message)
raise NotImplementedError
end
end
3. ✅ Use Adapters for External Systems
All external system access (e.g., database, email, payment gateway) should go through an adapter.
# adapters/repositories/active_record_order_repository.rb
class ActiveRecordOrderRepository
def save(order)
OrderRecord.create!(user_id: order.user_id, total: order.total)
end
end
4. ✅ Use Constructor Injection for Dependencies
Pass ports (interfaces) into use case classes via constructor injection.
# controller
use_case = CreateOrder.new(
order_repository: ActiveRecordOrderRepository.new,
notifier: EmailNotifier.new
)
use_case.call(...)
5. ✅ Test Core Logic in Isolation
Mock all adapters when testing core logic. Never hit the database or send emails in unit tests.
# RSpec
let(:repo) { instance_double("OrderRepository", save: true) }
let(:notifier) { instance_double("Notifier", send: true) }
6. ✅ Use Plain Ruby Objects (POROs) for Entities
Your domain models should be framework-independent and only represent business data.
# domains/order.rb
class Order
attr_reader :user_id, :items, :total
def initialize(user_id:, items:)
@user_id = user_id
@items = items
@total = items.sum { |i| i[:price] * i[:quantity] }
end
end
7. ✅ Keep Use Cases Focused
Each use case should do one thing: place order, send email, register user. Avoid mixing multiple responsibilities.
# app/core/use_cases/send_invoice.rb
class SendInvoice
def initialize(invoice_repo, mailer)
@invoice_repo = invoice_repo
@mailer = mailer
end
def call(invoice_id)
invoice = @invoice_repo.find(invoice_id)
@mailer.send_invoice(invoice)
end
end
8. ✅ Organize Code by Role, Not Layer
Structure your app by roles (core, ports, adapters), not traditional MVC folders. This improves separation and clarity.
app/
├── core/
├── ports/
├── adapters/
├── domains/
├── controllers/
9. ✅ Document Ports and Adapters Clearly
Explain what each port is responsible for, and which adapter implements it. This is critical when scaling or onboarding new devs.
10. ✅ Use Hexagonal Only Where It Makes Sense
For simple CRUD, you may not need full hexagonal structure. Start applying it to complex workflows and critical business logic first.
🌍 Real-World Use Case
- 🛍 E-commerce: Placing orders via web or API using the same `CreateOrder` use case
- 📬 Email & SMS: Notifications can be swapped by changing the adapter
- 📊 Analytics: Core logic tracks metrics through a `Tracker` port, not tied to a specific tool
Microservices in Rails (Architectural Pattern)
🧠 Detailed Explanation – Microservices in Rails
Microservices is an architectural pattern where a large application is split into small, independent services. Each service focuses on a single feature or business task — like managing users, handling orders, or sending emails.
Instead of building one big Rails app (called a monolith), you create multiple small Rails apps that talk to each other via APIs (HTTP) or background jobs (Sidekiq, Redis).
🏗️ What it looks like:
- User Service: Handles login, registration, authentication
- Order Service: Manages cart, checkout, payments
- Notification Service: Sends email, SMS, and in-app messages
Each service has its own codebase, its own database, and can be deployed separately.
🚀 Why Use Microservices?
- ✅ Services can be deployed independently
- ✅ Smaller codebases are easier to manage
- ✅ Teams can work on different services without stepping on each other
- ✅ Easier to scale parts of your system (e.g., just the order service)
❗ Things to Be Careful About
- ⚠️ More services = more complexity (especially communication)
- ⚠️ Requires good monitoring, logging, and DevOps skills
- ⚠️ Difficult to manage transactions across services
🔗 How Services Communicate
- Synchronous: REST APIs or GraphQL (e.g., `POST /api/orders`)
- Asynchronous: Background jobs, message queues (e.g., RabbitMQ, Kafka, Sidekiq)
# Example: Calling another service
HTTParty.post("http://notification-service.local/send",
body: { email: "user@example.com", message: "Order confirmed" }.to_json)
📦 Summary
- ✅ Microservices = many small apps that do one thing well
- ✅ Services talk to each other via APIs or queues
- ✅ Each service has its own database
- ✅ Best for teams, scale, and complex systems
📘 Key Terms & Concepts – Microservices in Rails
🔖 Term | 📄 Description |
---|---|
Microservice | A small, independent Rails app focused on one business feature (e.g., users, orders). |
Monolith | A single large Rails application that contains all features in one codebase. |
API Gateway | A central entry point that routes client requests to different services (optional but useful). |
Service-to-Service Communication | How microservices talk to each other — usually via REST APIs or background jobs. |
Service Discovery | How services find each other using internal DNS or a service registry (like Consul or Kubernetes). |
Database per Service | Each service has its own database to stay independent and avoid tight coupling. |
Event-Driven Architecture | Microservices communicate by publishing and listening to events (e.g., using Redis, Kafka). |
JWT (JSON Web Token) | A token-based auth system used to share login sessions across services. |
Service Boundary | The limit of responsibility for a service. Each service owns its logic and database. |
Sidekiq / Redis | Used for background job processing and async communication between services. |
Docker | A container platform that packages services and runs them independently. |
Kubernetes | Used to orchestrate and manage deployment of microservices at scale. |
🔁 Flow Section – How Microservices Work in Rails
Microservices follow a simple pattern: each service does one job well and talks to others via APIs or background jobs. Here's how the flow works:
- 1. User sends a request to the frontend or API Gateway
- 2. API Gateway routes the request to the correct microservice (e.g., to the
OrderService
) - 3. The microservice processes its own logic (e.g., creating an order, saving it in its own database)
- 4. If needed, the microservice calls other services (e.g., calls
NotificationService
to send an order confirmation email) - 5. Each service responds back with success or failure
- 6. The frontend or API Gateway shows the final response to the user
🧭 Example Use Case Flow (Ordering System)
- User clicks "Place Order" on the UI
OrderService
receives the request and creates the orderOrderService
notifiesPaymentService
to process payment- Once payment is done,
NotificationService
sends a confirmation email - Each service logs its action independently and stores its own data
🏢 Where We Use Microservices in Rails
- ✅ E-commerce platforms: Split cart, inventory, payments, and review systems
- ✅ Healthcare apps: Separate patient records, billing, appointments, and doctor profiles
- ✅ Messaging systems: Microservices for auth, chat, file upload, and analytics
- ✅ SAAS products: Billing, user accounts, notifications, and analytics services
- ✅ ERP systems: Finance, HR, CRM, and Supply Chain modules as microservices
📌 Summary of Flow
- 📦 Each microservice handles one function independently
- 🔗 Communication happens via APIs or message queues
- 🔒 Each service is isolated with its own data, logic, and deployments
- 💡 Failures in one service should not crash others (if designed well)
📦 Gems & Libraries Used in Rails Microservices
Microservices in Rails rely on a combination of Ruby gems and infrastructure tools to enable independent services, communication, and deployment.
🔧 Gem / Tool | 📄 Purpose |
---|---|
sidekiq | Background job processing between services (asynchronous communication). |
httparty | Simple HTTP requests to communicate with other microservices. |
faraday | A more advanced HTTP client for REST/GraphQL microservice requests. |
oj | High-performance JSON parsing when exchanging data between services. |
jwt | Secure token-based authentication between distributed services. |
graphql-ruby | Used if services expose or consume GraphQL APIs instead of REST. |
dotenv-rails | Manage service-specific environment variables for each microservice. |
Docker | Used to package and isolate each Rails microservice. |
PostgreSQL / Redis | Each service uses its own dedicated database; Redis often used for job queues or caching. |
Kubernetes | Orchestrates deployment, scaling, and service discovery across containers. |
Consul / etcd | Optional: Service discovery between microservices running across nodes. |
Note: Not all gems are required for every Rails microservice. Choose based on whether you're using REST, GraphQL, or background jobs.
🚀 Best Implementation – Microservices in Rails
This example shows how to break a large Rails monolith into 3 services:
- AuthService: Handles user signup, login, JWT
- OrderService: Handles product orders and payments
- NotificationService: Sends emails and SMS
🧱 Step-by-Step Implementation
- Create separate Rails apps: Each microservice is its own Rails app.
rails new auth_service --api rails new order_service --api rails new notification_service --api
- Each service has its own DB: Use PostgreSQL or MySQL.
config/database.yml
should be different per service. - Use JWT for authentication: AuthService issues tokens.
# AuthService: Generate JWT JWT.encode({ user_id: user.id }, Rails.application.secrets.secret_key_base)
- OrderService validates token:
# Decode token in a before_action token = request.headers["Authorization"].split.last decoded = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
- Use
HTTParty
orFaraday
to call other services:# OrderService -> NotificationService HTTParty.post("http://localhost:3003/api/v1/notifications", body: { email: user.email, message: "Order placed" }.to_json, headers: { "Content-Type": "application/json" })
- Use Sidekiq for async communication: When you don’t need an immediate response.
# In OrderService NotificationJob.perform_later(user.email, "Order received")
- Run services using Docker:
# Dockerfile FROM ruby:3.2 WORKDIR /app COPY . . RUN bundle install CMD ["rails", "server", "-b", "0.0.0.0"]
- Optional: Use Kubernetes or Docker Compose to manage all services together.
📦 Folder Structure for One Microservice
order_service/
├── app/
│ └── controllers/
│ └── api/
│ └── v1/
│ └── orders_controller.rb
├── jobs/
│ └── notification_job.rb
├── config/
├── Gemfile
├── Dockerfile
✅ Tips for Clean Design
- ✅ Each service is testable and deployable separately
- ✅ Use internal API keys or JWT for service communication
- ✅ Use message queues for scalability (e.g., Sidekiq + Redis)
- ✅ Avoid shared databases across services
📌 Final Output Example
User signs up at AuthService
→ gets token → places order at OrderService
→ which calls NotificationService
to send confirmation email.
💡 Example
Split a Rails monolith into:
- Auth Service: Manages users, login, OAuth
- Order Service: Handles orders, payments
- Notification Service: Sends emails/SMS
# Example communication (Auth Service -> Order Service)
HTTParty.post("http://order-service.local/api/orders", body: order_params.to_json)
🔁 Alternative Concepts
- Monolith: One big Rails app (easier for small teams)
- Modular Monolith: Structured modules in one app, with internal boundaries
- SOA: Service-Oriented Architecture (more loosely coupled than microservices)
💬 Technical Questions and Answers – Microservices in Rails
🔹 Q1: How do you communicate between two Rails microservices?
A: You use HTTP (via HTTParty
or Faraday
) for synchronous communication, or background jobs (Sidekiq
+ Redis
) for asynchronous messaging.
# OrderService sends a POST request to NotificationService
HTTParty.post("http://notification_service:3003/api/send",
body: { email: "user@example.com", message: "Order Confirmed" }.to_json,
headers: { "Content-Type": "application/json" })
🔹 Q2: How do you handle authentication across services?
A: Use JWT (JSON Web Tokens) issued by the AuthService and verified by other services.
# AuthService: create token
token = JWT.encode({ user_id: user.id }, Rails.application.secrets.secret_key_base)
# OrderService: decode token
decoded = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
🔹 Q3: Can each microservice have its own database?
A: Yes. This is a core principle of microservices — each service must own its data to stay independent.
Use isolated PostgreSQL
, MySQL
, or SQLite
instances per service.
🔹 Q4: How do you test service interactions?
A: Use contract testing or mocks. Example: RSpec with WebMock to stub external service calls.
# Stub NotificationService call in OrderService spec
stub_request(:post, "http://notification_service:3003/api/send")
.to_return(status: 200, body: "", headers: {})
🔹 Q5: What are common tools for orchestrating Rails microservices?
A: You can use:
- Docker Compose: For local development
- Kubernetes: For production orchestration
- NGINX or API Gateway: To route traffic to services
🔹 Q6: How do you avoid tight coupling between services?
A: Use asynchronous messaging (e.g., Redis queues, Kafka), and never share databases or models between services.
# In OrderService
NotificationJob.perform_later(user.id, "Your order is confirmed!")
🔹 Q7: How can services discover each other in dynamic environments?
A: Use internal DNS (in Docker Compose or Kubernetes), or tools like Consul, etcd, or service registries.
# In Docker Compose
notification_service:
build: .
ports:
- "3003:3000"
# Services access it using: http://notification_service:3000
🔹 Q8: How do you handle shared logic between microservices?
A: Avoid sharing code directly. If necessary, extract logic into a Ruby gem or shared API that services can consume independently.
# Create a shared gem
module CurrencyConverter
def self.usd_to_eur(amount)
amount * 0.92
end
end
✅ Best Practices – Microservices in Rails
Microservices offer flexibility and scale, but can also bring complexity. Below are proven best practices to follow when using Rails for microservices — along with real examples.
1️⃣ One Responsibility per Service
Each service should manage one business function. Avoid the temptation to add unrelated features.
# Good: user_service handles only authentication & registration
# Bad: user_service also sends emails, handles payments
2️⃣ Separate Databases per Service
Never share databases. Let each service own its own data to avoid tight coupling.
# Good: AuthService has its own PostgreSQL DB
# Bad: AuthService and OrderService write to the same DB table
3️⃣ Use JWT for Stateless Authentication
Issue a token once from AuthService and verify it in other services without sharing sessions.
# AuthService
JWT.encode({ user_id: 1 }, Rails.application.secret_key_base)
# Other service
JWT.decode(token, Rails.application.secret_key_base)
4️⃣ Prefer Asynchronous Messaging
Don’t block the user flow by waiting for other services. Use background jobs and message queues.
# Good: Notify via Sidekiq
NotificationJob.perform_later("Order confirmed")
# Bad: Wait for email service to respond during checkout
5️⃣ Use API Gateways or Internal DNS
Route external traffic using an API Gateway. Use internal hostnames (Docker/Kubernetes) for service discovery.
# In Docker Compose
http://notification_service:3000/api/send
6️⃣ Use Semantic Versioning for APIs
Version your APIs clearly to avoid breaking other services unexpectedly.
# Good:
POST /api/v1/orders
# Future:
POST /api/v2/orders
7️⃣ Avoid Overcommunication
Only call another service when necessary. Duplicate minimal data if needed to reduce calls.
# Good: Save order_summary in local DB
# Bad: Fetch user details from AuthService on every request
8️⃣ Fail Gracefully
Always handle errors between services to avoid cascading failures.
begin
HTTParty.post("http://notification_service/api")
rescue => e
Rails.logger.error("Notification failed: #{e.message}")
end
9️⃣ Use Health Checks and Monitoring
Set up a `/health` route in each service for liveness checks. Use Prometheus or CloudWatch for metrics.
# config/routes.rb
get "/health", to: proc { [200, {}, ["OK"]] }
🔟 Dockerize Every Service
Each Rails microservice should be containerized for isolation, easy deployment, and scaling.
# Dockerfile
FROM ruby:3.2
WORKDIR /app
COPY . .
RUN bundle install
CMD ["rails", "server", "-b", "0.0.0.0"]
🌍 Real-World Use Cases
- 🛍 E-commerce: Separate services for users, orders, payments, reviews
- 💬 Messaging Platforms: Chat, auth, analytics, and notifications as separate apps
- 🏥 Healthcare: Patient management, scheduling, and billing split into services
Learn more about Rails setup
https://shorturl.fm/6539m
https://shorturl.fm/XIZGD
https://shorturl.fm/ypgnt
https://shorturl.fm/47rLb
https://shorturl.fm/I3T8M
https://shorturl.fm/TDuGJ
https://shorturl.fm/Xect5
https://shorturl.fm/DA3HU
https://shorturl.fm/ypgnt
https://shorturl.fm/PFOiP