Service Objects are probably a single most popular technique for refactoring Ruby applications. However, there is one little with them: there are (too) many ways to write them. And because different people use service objects different way, they tend to get messy too.

I wrote dozens of SOs since I joined Boostcom 1.5 year ago. This is mostly because we don’t really use Rails, so they are even more natural. We never established strict rules to follow while writing services, however, they all tend to look similar. It would seem that the way we write them works for us, so I’d like to share a few tips about what I think service objects should look like.

Here is a list as bullet points, with longer explanation coming later in the post:

Only one public method

Use .new explicitly

explicitly Only use instance variables in a constructor

Don’t overuse private methods (stay flat)

Don’t be afraid of local variables

Reuse service instances in heavy loops

Bonus: Return value objects

Bonus level hard: Use monads

Break the rules

Only one public method

Since classes generally should follow Single Responsible Principle, we should apply it to service objects too. One responsible means only one public method – and one way to use the service.

I usually call it simply call . This has a few benefits:

You can use Ruby magical shortcut syntax service.(arg) . This might not be a killer feature, but sometimes is nice to have

. This might not be a killer feature, but sometimes is nice to have If method’s name is call , you need to give a proper name to a service, which is good

, you need to give a proper name to a service, which is good “Calling” services seems natural

I know other people who have completely different opinion on this subject and say you should never call your method call . This is, however, not really important, as long as there is only one method.

Use .new explicitly

I know many people like writing a shortcut like this:

class MyService def self . call ( * args ) new ( * args ) end def call ( args ) # do something end end

I admit: there is a certain appeal to writing MyService.call(1,2,3) instead of longer and seemingly redundant MyService.new.call(1,2,3) . It’s certainly shorter and you usually don’t reuse service objects instances anyway. I actually don’t think it’s the best way.

On the other side lies this counterproposal: MyService.new(my_secret_data).call . I must say I use it sometimes, but I also think this is not perfect. Why? By passing the data to initializer and assigning to an instance variable, you are later tempted to reuse the service with a different method (and same data). This breaks the previous rule about only one public method. For example:

service = MyService . new ( data ) service . remove_dulicates! filtered = service . filter_invalid service . send_valid_to_api ( filtered )

I wouldn’t dare say that this is bad per se but it allows your service objects to grow uncontrollably. If you have self-discipline to avoid that, this might be your way. But I think there are other possibility worth exploring.

At some point in your service-objects-writing quest, you will probably discover dependency injection. And you will want to rewrite the services like that:

# before class MyService def call ( data ) filtered = FilteringService . new . call ( data ) ApiSender . new . call ( filtered ) # where are my Elixir pipes? :( end end # after class MyService def initialize ( filterer: FilteringService . new , api_sender: ApiSender . new ) @filterer = filtering @api_sender = api_sender end def call ( data ) filtered = filterer . call ( data ) api_sender . call ( filtered ) # where are my Elixir pipes? :( end private attr_reader :filterer , :api_sender end

Why? For example for testing MyService without needing to stub both services it depends on. But that’s a whole different story… Anyway, by leaving constructor unused at first, it is really easy to put dependency injecting code there later. That’s why I think that MyService.new.call is a way to go if you don’t have any other strong opinion.

Only use instance variables in a constructor

As seen in the example above, instance variables are only used in the constructor. This is a way it should be, in my opinion. If you use them in any other place, user attr_reader and avoid mutating those variables at any cost.

There might be a question whether those getters should be private or not. I think they should, but it’s not really a big deal.

One more hint: use dry-initializer which does the job for you.

require 'dry-initializer' class MyService extend Dry :: Initializer param :filterer , default: proc { FilteringService . new } param :api_sender , default: proc { ApiSender . new } def call ( data ) filtered = filterer . call ( data ) api_sender . call ( filtered ) end end

Don’t overuse private methods

If your service is getting complicated (and sometimes it has to), you will add more and more private methods to keep things short and Rubocop happy. In time, the code will become hard to read. Look at this example:

class BadService def call ( data ) send_filtered_data_to_api ( data ) notify_subscribers ( data ) end private def notify_subscribers ( data ) SubscribersNotifier . new . call ( data ) end def send_filtered_data_to_api ( data ) api_sender . call ( filtered ( data )) end def filtered ( data ) filterer . call ( data ) end def api_sender @api_sender ||= ApiSender . new end def filterer @filtering_service ||= FilteringService . new end

How many jumps you had to do in order to read what this code does? The answer is: a lot call -> send_filtered_data_to_api -> api_sender -> send_filtered_data_to_api -> filtered -> filterer -> filtered -> send_filtered_data_to_api -> call -> notify_subscribers -> call . Or using a different notation:

call send_filtered_data_to_api api_sender filtered filterer notify_subscribers

As you see, we have four levels of calls here. I think it only should be two of them. That’s why I also sometimes call this rule “stay flat”.

In my opinion, a perfect way is to only use private methods in call . It means both: nothing else than private methods in call and only call can use private methods. This way you keep the entry method as a kind of high-level description of what service does. Of course, private methods should be defined in the same order that they are called, so you don’t need to refer to what call does when you read them.

This way even services that do a lot can stay simple to read:

class RequestHandler def call ( params ) valid_params = filter_valid_params ( params ) save_to_database ( valid_params ) notify_subscribers ( valid_params ) log_params ( params ) send_measures ( params ) end private # [...] end

Sidenote: if your private methods start to do too much and be too long, you probably need to introduce another service.

Don’t be afraid of local variables

Earlier this year I’ve read about a thing called The Local Variable Aversion Antipattern and I started to notice it a lot in Ruby code.

Basically, you don’t need to initialize everything in a separate method. Also, there’s no need to memoize everything – usually you use it only once anyway. So there is nothing better in this:

def send_to_api ( data ) api_sender . call ( data ) end def api_sender @api_sender ||= ApiSender . new end

Than this:

def send_to_api ( data ) sender = ApiSender . new sender . call ( data ) end

The latter is actually easier to follow and does not break the rule about setting instance variable only in initializers.

Reuse service instances in heavy loops

This is probably pretty obvious, but we talk very little about performance when it comes to high-level Ruby code, so I dare to say it aloud.

# DON'T large_array . each do | item | ItemProcessor . new . call ( item ) end # DO processor = ItemProcessor . new large_array . each do | item | processor . call ( item ) end

Ruby objects are relatively cheap, but they are not free. If you can avoid creating unnecessary objects, you’ll benefit from it at some point.

Bonus: Return value objects

It is a good habit to return service objects when you do something more complicated than just streamlining data. At some point you will like to know more about what happened in high-level service objects and you will start to create little monsters like this:

class MyHighLevelService def call ( data ) pg_result = save_to_postgresql ( data ) kafka_result = send_to_kafka ( data ) worker_ids = schedule_workers ( data ) [ pg_result , kafka_result , worker_ids ] end end # ... pg_result , kafka_result , worker_ids = MyHighLevelService . new . call ( data )

Value objects are much better for it.

Bonus level hard: Use monads

I’m not going to say much more, as this is more of a point on my checklist to start doing. But sometimes what dry-monads offer will very easily fit your flow of data. But don’t try to squeeze them everywhere just because “monads are cool”. As Piotr Solnica once said:

One more thing re monads - I totally recommend using dry-monads in places where it’s useful, like composing operations with nice result handling etc. Something that would be “too much”, is using monads consistently everywhere, for everything. — Piotr Solnica (@_solnic_) June 26, 2018

Last but not least: Break the rules if necessary

This is just a set of suggestions. I think they are good and cohesive, but they are suggestions anyway. Even I don’t always follow them. Sometimes there are good reasons not to. For example, when all other things (for example coming from external gems you have no control over) are using Blah.call(1) , maybe your services should use that notation too? Otherwise you will find yourself wondering everytime what to write.

Don’t start changing everything in your code just because some dude on the Internet says he does it otherwise. Make conscious decisions, try to adhere to them, but give yourself permission to sometimes stray away. Context is everything, after all, and yours can be very different than mine.