DEV Community

Alexander Shagov
Alexander Shagov

Posted on • Edited on

Ruby on Rails: Rethinking Service Objects

In the Ruby on Rails community, service objects have gained popularity as a way to encapsulate business logic and keep controllers and models lean. However, there's a growing trend of misusing service objects, particularly those with "-er" or "-or" suffixes. This article aims to shed light on poorly implemented service objects and provide guidance on creating more effective, domain-focused alternatives.


Many developers tend to name their service objects with suffixes like -er or -or, such as ArticleCreator or UserAuthenticator. While this naming convention may seem intuitive, it often leads to a procedural style of programming rather than embracing the object-oriented nature of Ruby.

It's becoming evident that service objects the way we often see them are not actually "objects", but just a procedures.. or better to say, processess.

Let's take a look at the real-world example of a codebase powering the dev.to community: https://github.com/forem/forem/blob/main/app/services/articles/creator.rb.
The class seems okay at first glance; at least the code is organized and lives in its own space. It could be workable. However, we're just hiding the problem in the long term.

# The code structure is taken from https://github.com/forem/forem/blob/main/app/services/articles/creator.rb

class ArticleCreator
  def initialize(user, article_params)
    @user = user
    @article_params = article_params
  end

  def call
    rate_limit!
    create_article
    subscribe_author
    refresh_auto_audience_segments if @article.published?
    @article
  end

  private

  def rate_limit!
    # Rate limiting logic
  end

  def create_article
    @article = Article.create(@article_params.merge(user_id: @user.id))
  end

  def subscribe_author
    NotificationSubscription.create(user: @user, notifiable: @article, config: "all_comments")
  end

  def refresh_auto_audience_segments
    @user.refresh_auto_audience_segments
  end
end
Enter fullscreen mode Exit fullscreen mode

A Domain-Centric Approach

Instead of focusing solely on actions (creating, updating, deleting), we should model our service objects around domain concepts and processes. Hence my suggestion: let's stop pretending service objects are objects and start calling them processess instead.
NOTE: I'm not talking about DDD (Domain-Driven design) since it's a whole separate topic, however, we can be inspired by some of the concepts.

I'll be using dry-* libraries to demonstrate how a process might have looked like:

module Articles
  class PublishingProcess
    include Dry::Transaction

    step :validate
    step :rate_limit
    step :create_article
    step :subscribe_author
    step :refresh_auto_audience_segments
    step :notify_subscribers

    private

    def validate(input)
      contract = Articles::ValidationContract.new
      result = contract.call(input)
      result.success? ? Success(input) : Failure(result.errors)
    end

    def rate_limit(input)
      limiter = Articles::PublishingQuota.new(input[:user])
      limiter.within_limit? ? Success(input) : Failure(:rate_limited)
    end

    def create_article(input)
      repository = Articles::Repository.new
      article = repository.create(input[:user], input[:params])
      Success(input.merge(article: article))
    end

    def subscribe_author(input)
      subscription = Articles::AuthorSubscription.new(input[:user], input[:article])
      subscription.create
      Success(input)
    end

    def refresh_auto_audience_segments(input)
      # NOTE: the naming might not be the best one for this case since I'm not aware about the actual business logic behind
      audience = Articles::Audience.new(input[:user])
      audience.refresh_segments
      Success(input)
    end

    def notify_subscribers(input)
      publication = Articles::SubscriberNotification.new(input[:article])
      publication.dispatch
      Success(input)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

See, not only the business process is very well defined, but it also deals with domain-centric objects: Articles::ValidationContract, Articles::PublishingQuota, Articles::Repository, Articles::AuthorSubscription and etc.


⭐ Thanks to having a clear separation between Processes and Domain objects we'll stop pretending that we play in OOP game when dealing with service objects.

Let's imagine an ideal world where each app consisted of Process-like implementations. As a result, our app might look like a set of business processes. One process might start another as an async job, or some processes might be executed in the background on scheduled basis. Yet, it doesn't change the fact that an app consists of a set of business processes.

# Hotel Booking Process
module Bookings
  class HotelReservationProcess
    include Dry::Transaction

    step :check_room_availability
    step :calculate_total_cost
    step :process_deposit
    step :create_reservation
    step :send_confirmation
    step :schedule_reminders
  end
end
Enter fullscreen mode Exit fullscreen mode
# User Registration Process
module Users
  class RegistrationProcess
    include Dry::Transaction

    step :validate_user_data
    step :check_unique_email
    step :create_user
    step :send_verification_email
    step :assign_default_roles
  end
end
Enter fullscreen mode Exit fullscreen mode

I think it goes without saying that having a clear separation of processes and a set of domain-centric objects powering these processes is a cleaner way to write business logic. However, I understand that the "ideal" world and the "real" world are sometimes completely different pictures. It always boils down to engineering contracts established in a particular team or the whole organization, and sometimes it's not that easy to change habits, especially when we're talking about legacy production applications.

Nevertheless, what we can do is refine our mental models to try to perceive "business processes" instead of "some" objects, and incorporate the business language into the domain language of our system.

Top comments (0)