class ApplicationCommand
Result = Struct.new(:value, :errors) do
def success?
errors.nil? || errors.empty?
end
def failure?
!success?
end
end
def self.call(*args, **kwargs)
new(*args, **kwargs).perform
end
def perform
raise NotImplementedError, "#{self.class} must implement #perform"
end
private
def success(value = nil)
Result.new(value, nil)
end
def failure(errors)
Result.new(nil, Array(errors))
end
end
class RegisterUser < ApplicationCommand
def initialize(params)
@params = params
end
def perform
user = nil
ActiveRecord::Base.transaction do
user = User.new(@params)
unless user.save
return failure(user.errors.full_messages)
end
workspace = user.workspaces.create!(name: "#{user.name}'s Workspace")
user.update!(default_workspace: workspace)
end
WelcomeMailer.with(user: user).welcome.deliver_later
success(user)
rescue ActiveRecord::RecordInvalid => e
failure(e.record.errors.full_messages)
end
end
class UsersController < ApplicationController
def create
result = RegisterUser.call(user_params)
if result.success?
render json: UserSerializer.new(result.value), status: :created
else
render json: { errors: result.errors }, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
end
Fat controllers accumulate validation, persistence, side effects, and branching until they become untestable and impossible to reuse. The command object pattern extracts a single business operation into a plain Ruby class that takes its inputs, performs the work inside a transaction, and returns a structured result. The controller is left with one job: translate an HTTP request into a call and translate the result back into a response.
The ApplicationCommand base class defines the contract. Its call class method instantiates the command and invokes perform, wrapping the whole thing so that subclasses never manage boilerplate. The success and failure helpers return a small Result struct carrying a value and an errors collection, plus a success? predicate. This is a lightweight result type rather than a full monad — it avoids raising for expected domain failures, which keeps normal control flow out of exception handling and makes both branches explicit at the call site.
In RegisterUser command, perform runs inside ActiveRecord::Base.transaction so that partial work is rolled back on any failure. It builds the User, returns failure(user.errors) when the record is invalid, then performs genuine side effects: provisioning a default Workspace and enqueuing WelcomeMailer via deliver_later. Because the mail is enqueued only after the transaction concerns are satisfied, the command avoids sending mail for a user that was never really saved. The whole operation is idempotent from the caller's perspective — it either fully succeeds or reports errors.
UsersController shows the payoff. create calls RegisterUser.call(user_params) and branches on result.success?, rendering the new user or the errors with an appropriate status. There is no business logic, no transaction management, and no knowledge of mailers or workspaces — those details live in one place that a background job or rake task could reuse without touching HTTP.
The trade-off is more classes and a little indirection, which is overkill for trivial CRUD. The pattern pays off once an action touches multiple models, has side effects, or is invoked from several entry points, because the operation becomes independently testable and the failure contract is uniform everywhere it is used.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.