ruby 71 lines · 3 tabs

Keep Controllers Thin: Use Command Objects

Shared by codesnips Jan 2026
3 tabs
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
3 files · ruby Explain with highlit

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

Share this code

Here's the card — post it anywhere.

Keep Controllers Thin: Use Command Objects — share card
Link copied