My take on services in Rails
It’s been a while since my last post. I was writing my engineer’s thesis which caused general disgust towards writing at all. Anyway, these sad times are over and here comes the shiny new blogpost about introducing service layer in Rails applications. It does not contain any breakthrough thoughts, but is rather a mixture of ideas I learned from great Ruby developers.
Why?
We need a place to store the domain logic of our app. If you follow The Rails Way it can be:
a) controller
b) model
Controller is an entry point to our application. However, it’s not the only possible entry point. I would like to have my logic accessible from:
- Rake tasks
- background jobs
- console
- tests
If I throw my logic into a controller it won’t be accessible from all these places. So let’s try “skinny controller, fat model” approach and move the logic to a model. But which one? If a given piece of logic involves User
, Cart
and Product
models - where should it live?
A class which inherits from ActiveRecord::Base
already has a lot of responsibilities. It handles query interface, associations and validations. If you add even more code to your model it will quickly become an unmaintainable mess with hundreds of public methods. How can you change one line of your model’s code with confidence that nothing breaks up? It’s much easier when the whole class fits into a single screen of code.
How to implement a service?
A service is just a regular Ruby object. Its class does not have to inherit from any specific class. Its name is a verb phrase, for example CreateUserAccount
rather than UserCreation
or UserCreationService
. It lives in app/services
directory. You have to create this directory by yourself, but Rails will autoload classes inside for you.
class CreateUserAccount
def call(params)
[...]
end
end
The service does one thing and has one public method. I used to name it perform
, but call
is slightly better. Lambda also responds to call
so in your tests you have the possibility to mock service with lambda, which is quite convenient.
Splitting big services
As you implement a service object you may notice that it grows in size. It is usually a sign that this service has more than one responsibility and should be composed of a few smaller services. How to organize them? Here comes the example.
In app/services/create_user_account/generate_token.rb
:
class CreateUserAccount
class GenerateToken
def call(user)
[...]
end
end
end
In app/services/create_user_account/send_welcome_email.rb
:
class CreateUserAccount
class SendWelcomeEmail
def call(user)
[...]
end
end
end
In app/services/create_user_account.rb
:
class CreateUserAccount
def call(params)
[...]
generate_token.call(user)
send_welcome_email.call(user)
[...]
end
end
So you have to create a new directory app/services/create_user_account
and place there those extracted services. Each of them should be encapsulated in CreateUserAccount
namespace. Again, Rails autoloads everything for you.
Dependency injection
How can you obtain instance of this “child service” in your “parent service”? You could write something similar to:
class CreateUserAccount
def call(params)
[...]
generate_token = GenerateToken.new
generate_token.call(user)
[...]
end
end
In such a case you hardcode instantiation of GenerateToken
service inside CreateUserAccount
. Your are not able to easily provide mock implementation of GenerateToken
inside your tests so you cannot test CreateUserAccount
service in isolation.
The simple yet powerful solution is described here. Let’s apply it in our case:
class CreateUserAccount
def self.build
new(GenerateToken.build, SendWelcomeEmail.build)
end
def initialize(generate_token, send_welcome_email)
@generate_token = generate_token
@send_welcome_email = send_welcome_email
end
def call(params)
[...]
@generate_token.call(user)
[...]
end
end
class CreateUserAccount
class GenerateToken
def self.build
new
end
[...]
end
end
class CreateUserAccount
class SendWelcomeEmail
def self.build
new
end
[...]
end
end
We pass all dependencies to the constructor of the service. But we also provide a factory method - build
which knows the sane way to build the service.
Complete example
Here is the complete example which combines everything:
class CreateUserAccount
def self.build
new(GenerateToken.build, SendWelcomeEmail.build)
end
def initialize(generate_token, send_welcome_email)
@generate_token = generate_token
@send_welcome_email = send_welcome_email
end
def call(params)
# code which creates user model
[...]
@generate_token.call(user)
@send_welcome_email.call(user)
user
end
end
class CreateUserAccount
class GenerateToken
# this service has no dependencies
def self.build
new
end
def call(user)
[...]
end
end
end
class CreateUserAccount
class SendWelcomeEmail
# this service has no dependencies
def self.build
new
end
def call(user)
[...]
end
end
end
Update 27.12.2014
Following suggestions in the comments, I’ve prepared a dummy app to demonstrate usage of service objects. I hope it’s at least a bit more concrete than the examples above. Here is the link: https://github.com/adamniedzielski/service-objects-example
Update 06.05.2019
Four and half years after writing this blog post and the Rails community still does not have a standard way of writing service objects :) You can read what my fellow programmer Paweł thinks about service objects in Rails.
Update 17.04.2021
This blog post remains one of my personal favorites! The approaches presented in this blog post still hold for me and I reach for them in new projects.
There’s one syntactical change that I made after the feedback of my
colleagues from Liefery. That’s using keyword arguments with default values
in initialize
which eliminates the need for the separate build
method.
Here’s the last example rewritten:
class CreateUserAccount
def initialize(generate_token: GenerateToken.new, send_welcome_email: SendWelcomeEmail.new)
self.generate_token = generate_token
self.send_welcome_email = send_welcome_email
end
def call(params)
# code which creates user model
[...]
generate_token.call(user)
send_welcome_email.call(user)
user
end
private
attr_accessor :generate_token, :send_welcome_email
end
class CreateUserAccount
class GenerateToken
def call(user)
[...]
end
end
end
class CreateUserAccount
class SendWelcomeEmail
def call(user)
[...]
end
end
end
You can see this applied in practice, for example, here. And tests become very explicit and simple like here.