How Service Objects in Rails Will Help You Design Clean And Maintainable Code

Photo of Tomek Pewiński

Tomek Pewiński

Updated Feb 27, 2024 • 9 min read
What is service object in Ruby on Rails?

If you’re following the trends in Ruby on Rails, you’ve probably heard the word ‘service’ a few times, or perhaps even encountered it in code that lives in the app/services directory.

In this post I’ll try to clarify what a service is, when it’s useful, and the different kinds of services I’ve used previously.

What is a service object in Ruby on Rails?

Service object is plain old Ruby Objects (PORO) and is typically responsible for carrying out some action. Service object in Rails implements the user’s interactions with the application. It contains business logic that coordinates other artefacts. You could say it is the core of the application.

In fact, inspecting the services folder of an application should tell the programmer what the application really does, which is not always obvious when looking at controllers or models.

Let’s take a look at an example from a Rails app:

We can see that it’s some sort of invoicing application. We could deduce this from seeing User and Invoice models, but looking at services like this tells us much more: we know the exact paths of user interaction within the application. We know it allows users to create invoices, correct them, and pay for them, and additionally register with their Google account and change passwords.

Ruby on Rails services has the benefit of concentrating the core logic of the application in a separate object, instead of scattering it around controllers and models.

Let’s take a look at how one could go about implementing a service.

<p data-gist-id="78195c90313078e8a126">&nbsp;</p>

Implementation of service objects

The common characteristic among all services is their lifecycle:

  • accept input
  • perform work
  • return result

However, this definition is quite broad. Let’s go one-by-one through each of the stages to see the specifics.

Initialization

Because a service implements user interaction, it is typically initialized with the user object. In the web application, this is the user that makes the request. Additional initialize arguments might include other context information if applicable. These would be things like current_company, service dependencies (more on that later) and user input.

Input

The service object accepts user’s input (in a web application, this might be submitted form or JSON payload). In the application code, input can take many forms:

Single value

The simplest case, but rarely seen.

Hash of values

For example, params from a rails controller. Common and simple to use. However, has the drawback of binding service to input format (for example, if input format changes, service internals must also change).

Form Object

A separate object which represents user input. It handles the parsing and validating the input format, freeing the service from doing it. It’s useful to decouple parsing complex params from actually performing work in the service.

For example, it can convert three separate fields in params to a single Time object, which is much easier to work with in the application code:

Work

The magic happens when you call a service. This is typically accomplished by sending a call message to the service instance. Alternatively, you can use a different method name if it makes sense in your domain.

(however, if the method names end up being something meaningless, like commence or invoke, it’s better to standardize on call (it’s also the method ruby Procs and lambdas use.)

The call method uses the data we passed on initialization and input data to perform service’s work. This includes, for example:

  • creating / updating / deleting a record
  • orchestrating the creation / updates of multiple records
  • delegating to other services for sending emails, notifications (more on that later)
  • … and whatever else your application does!

Finally, the method should return a result, which we’ll discuss next.

Result

The Service Object can perform complex operations. And as programmers, we know that when something can go wrong, sooner or later it will! Thus, we need a way to signal success or failure when using a service. I’ve noticed four ways in which that can be done:

Boolean value

In simple cases, just returning true for success and false for failure is enough (this is what ActiveRecord save method uses, for example). This return value, however, does not carry any additional information.

ActiveRecord Object

If the services role is to create or update rails models, it makes sense to return such an object as result. You then check for the presence of errors on the returned instance to decide if the call was a success.

If you’re developing a web API, you could pass such object directly to Rails’ respond_with method in the controller. It would figure out correct status codes for its own.

Status Object

We use small utility objects to signal success or error. This is helpful in most complex cases, for example when there are multiple objects created simultaneously, or many ways in which the operation may fail. This is what these objects would look like:

Raise an exception

We raise an exception on any kind of failure in the service object. Like status objects, exceptions can carry data, and have meaningful names in your domain.

Of course, when a call completes without raising an exception we treat it as a success.

(I’m not a fan of this solution though - In my opinion exceptions should be reserved for truly exceptional cases which don’t have domain meaning, i.e. network errors.)

That’s how I tend to implement service objects. I prefer to initialize the service with dependencies as well as input. It allows me to extract private methods inside the service which do not have to receive input as argument. There are other ways to structure the initialize-input-work-result pattern, which I’ve sketched here:

Using Services in application code

We’ve seen how a service object might be implemented in Ruby on Rails, now let’s take a look at its usage in application code.

You might suspect that services will be used on the boundary between user interface and application - and you’d be right! In the context of Rails, this boundary is the controller. An application using services would instantiate them in controller actions, tell them to perform work and respond back to the user. Let’s see an example:

Using controllers with services makes controllers really slim. All the business logic is encapsulated in services and models, and parsing input, in this case, in form object. This leaves controller as a thin layer of interaction between user and the application.

I’m most focused on writing web APIs these days, let’s see how we can use Service Objects, Status Objects and Rails’s Responders to produce a nice, consistent API:

Now this is an API I’d want to use! In this case, we’ve overrode Ruby on Rails’ default responder (the object handling respond_with call). We have clean error messages, codes for specific error cases and adherance to HTTP’s status codes (200 on success, 201 on created, 422 on error, like unsuccessful validation - this is handled by Responder). We could of course customize it even further, to account for other kinds of errors from our services. See ActionController::Responder code for details.

Testing Services

If services make the core of the application, it is crucial to test them properly. Luckily, since they are Plain Old Ruby Objects (POROs) they don’t have the heavyweight dependencies on framework code that models or controllers might have. This makes them easier to test.

In addition, we have greater control over their dependencies (recall that we pass them to the service object when initializing). This means we can pass mocks instead of concrete dependencies, or dummy implementations (like in-memory storage, instead db-based). This technique is called Dependency Injection. Let’s see it in action! I’m using rspec in this example:

Don’t forget that the service dependencies are not limited to objects passed on initialization. Any hard-coded constant (like a class name) or assumption about the interface is a dependency too.

However, we can use Dependency Injection to inject the required constants. If we’re dealing with database models, we can use repository pattern to do that.

(This is a long topic, out of the scope of this article. Check out Working with Repositories by Adam Hawkins for more information)

Using service objects to design clean code

Even though we had Ruby on Rails in mind in the course of this blog post, Rails is not a dependency of described service objects. You can use it with any web framework, mobile or console app. Read more about Ruby on Rails pros and cons here (non-developer friendly type of text).

That’s it! I hope I demonstrated how using service objects decouples concerns, simplifies testing and helps produce clean, maintainable code.

Further reading / watching:

I've also shared my thoughts on How Developing SPA Influenced Me & My Code.

Photo of Tomek Pewiński

More posts by this author

Tomek Pewiński

Tomek is one hell of a web developer and an open-source contributor. In between devouring popular...
Lost with AI?  Get the most important news weekly, straight to your inbox, curated by our CEO  Subscribe to AI'm Informed

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business