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.
As I understand it, a Service Object 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.
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.
The common characteristic among all services is their lifecycle:
However, this definition is quite broad. Let’s go one-by-one through each of the stages to see the specifics.
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.
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:
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:
Finally, the method should return a result, which we’ll discuss next.
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:
We’ve seen how a service object might be implemented, 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 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.
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)
Even though we had 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.
I've also shared my thoughts on How Developing SPA Influenced Me & My Code.