Use one service object per endpoint

Service objects can be used to share code, leading to nested service objects:

Problems πŸ”—

This makes service objects difficult to reason about.

Result objects also add extra complexity.

Solution πŸ”—

Use exactly one service object per endpoint.

Don’t nest service objects.

Use domain objects.

What is a domain object? πŸ”—

  1. Has data and behaviour
  2. Uses business language
  3. Can be a plain old Ruby object or an Active Record object (POROs preferred)

Where are domain objects stored? πŸ”—

Examples πŸ”—

Bad πŸ”—

class SearchesController < ApplicationController
  def update
    search = Search.find(params[:id])
    if params[:action] == "back"
      result = BackSearchStepService.new.call(search:)
      updated_search = result.data[:search]
      redirect_to search_step_path(updated_search.current_step)
    elsif search.current_step.final?
      result = CompleteSearchService.new.call(search:)
      updated_search = result.data[:search]
      redirect_to search_step_path(updated_search.current_step)
    else
      result = AdvanceSearchStepService.new.call(search:)
      if result.success?
        updated_search = result.data[:search]
        redirect_to search_step_path(updated_search.current_step)
      else
        redirect_to search_step_path(params[:step_id]), alert: I18n.t(result.errors.first)
      end
    end
  end
end

Good πŸ”—

class SearchesController < ApplicationController
  def update
    result = UpdateSearchService.new.call(request:)
    if result.success?
      redirect_to search_step_path(result.search.current_step)
    else
      redirect_to search_step_path(params[:step_id]), alert: I18n.t(result.errors.first)
    end
  end
end
class UpdateSearchService
  def call(request:)
    search_params = SearchParams.from_request(request)
    return ServiceObject::Error.new(["agent_finder.searches.invalid"]) if search_params.invalid?

    search = Search.find(search_params[:id])
    if search_params.back_pressed?
      search.go_back_a_step!
    elsif search.current_step.final?
      search.run!
      search.show_results!
    else
      search.advance_step!
    end
    ServiceObject::Success.new(search: search)
  end
end