Why You Need Service Objects

Ruby on Rails service objects

Posted by Theo Cha on August 11, 2022

Why You Need Service Objects?

The problems


As your application grows you will see domain/business logic scattered across models, controllers and GraphQL resolvers. many logics don’t belong to controllers and resolvers or models, making it difficult to reuse and maintain code.

1
2
3
4
5
class ProductController < ApplicationController
  def create
    Product.new(*args)
  end
end

This seems fine but as your application grows, things can easily become a mess. For example,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ProductController < ApplicationController
 def create
    default_args = { genre: find_category(), location: find_store_location() }
    Product.new(attrs.merge(default_args))
 end

 private

 def find_category
   // ...
 end

  def find_store_location
   // ...
 end
end

Service objects allow you to extract this logic into a separate class. This simplifies the code in a better way.

1
2
3
4
5
6
class ProductController < ApplicationController
  def create
    ProductCreator.create_product
  end
end

Benefits of encapsulating business logic:


  • Thin Rails controller and GraphQL resolver: they are mainly responsible for understanding requests and responses.
  • Re-usable services
  • Testable: Since the logic in a controller or resolver is thinner and simpler, it becomes really easy to test. also, testing business processes becomes more isolated. it is easier to stub and check whether specific processes are invoked within our service.

Implementation


First let’s create a new ProductCreator And then just dump all our logic inside a new Ruby class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ProductCreator
  def initialize(name:)
    @name = name
  end

  def create_product
    Product.create!(
      title: @name
    )
    rescue ActiveRecord::RecordNotUnique => e
     # handle duplicate entry
    end
  end
end

and then, we can call the service object within the application:

1
2
3
4
5
class ProductController < ApplicationController
  def create
    ProductCreator.new(name: params[:name]).create_product
  end
end

Service Object Syntactic Sugar


We can make the ProductCreator.new(arguments).create_product chain simpler by adding a class method that instantiates the ProductCreator and calls the create method for us:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ProductCreator
  def self.call(...)
    new(...).call
  end

  def initialize(name:)
    @name = name
  end

  def call
    Product.create!(name: @name)
    rescue ActiveRecord::RecordNotUnique => e
     # handle duplicate entry
    end
  end
end

and then, we can call the service object within the application in a simpler way:

1
2
3
4
5
class ProductController < ApplicationController
  def create
    ProductCreator.call(name: params[:name])
  end
end

References