Service Objects in Rails
Let’s kick off 2016 with a whistle-stop tour of one of my favourite OO approaches, Service Objects, in the context of Rails.
In Fowler’s incredible ‘Patterns of Enterprise Application Architecture’, he identifies the need of a service layer, that mediates between the domain model, and elements that need to interface with the business logic of the application. Unfortunately, Rails’ straight up MVC architecture doesn’t provide ‘out-of-the-box’ support for services – it expects your business logic to exist within your models or controllers (the latter being considered an anti-pattern). We can implement this service layer through service objects.
To begin with, let’s start out by defining what a service is and isn’t, and then look at how I like to implement service objects in Rails. Service objects are a hotly debated thing inside the Rails community, so fair warning, this is going to be opinionated.
What is (and isn’t) a Service Object
Programming is a relatively new field, with web and enterprise development even younger, so we don’t have a particualrly well defined canon of work that forms the vocabularly we use to discuss conscepts. As a result, service objects have been defined over and over, with little agreement. What follows is how I define a service object.
What Service Objects are not
Let’s start by defining what service objects are not, before giving them a positive description.
This is heavily opinionated, but I’m open to
discussion if you think otherwise; just drop me a line.
In an object-oriented world, we tend to think of ‘things’ as objects and how we act on ‘things’ as methods (or whatever word that describes a procedure acting on an object). A service is not a method: it is an action represented by an object, for example
SendReportEmail. The service handles the mediation between one or more objects, so isn’t part of another object’s behaviour.
2. Query/Finder objects
In my opinion, service objects are distinct from query objects, notably in that they have no return value. This means a service only deals with an action or actions, and only returns an exception when one is raised. The service is a bit of a diva, so will not be answering questions at this time.
This is in stark contrast to a query object, which returns records from a persistance store of some sort (RDBMS, document stores, etc).
3. The kitchen sink
Services can often become a dumping ground for many things, and can lose sight of their true purpose. i.e. if it isn’t a model, a controller or a view, it must be a service! They can become bloated, and a single service can end up doing too many things. I take a fairly strict view on how much a service can do, and what services encapsulate, but more on that in a bit.
What Service Objects are
1. Architecture independent
Service objects are an implementation of a specific pattern, usually the
command pattern, and
therefore are architecture and language indepdent. In Rails, I
think it is preferable to implement services as Plain Old Ruby Objects,
or ‘PORO’. The PORO is the secret weapon of a Rails application, hiding
in plain sight (or at least the
Service objects are, in my opinion, best as simple objects. Without getting too bogged down in implementation detail, I believe they should have a single entry point, and do exactly one thing: the name of the service.
3. Loosely coupled
Dependency injection is used with service objects to make sure they are loosely coupled. The service itself should have little to no bearing on the rest of the application, and ideally should not inherit from other objects. This has the side effect of being blazing fast to test, as you only need to set up the client object (as a test mock or the like).
I think this could probably be a ‘Rule #0’. It should be obvious what
a service does from the name of class.
NotifyUser sends a notification the the user,
UpdateOrder updates an order. Not only is this readable to
whoever is reading the codebase, it helps immensely when debugging as
the stack trace will make business sense, too.
On to the nitty-gritty – how do we implement these things anyway? We’ve set some ground rules above for services, but there are two addenda:
Services do not call other services
This is a pretty huge (and controversial) restriction. The reason for this is that a service that calls another service means it no longer has a single responsibility – it’s doing that service’s job too by calling it. I prefer to orchestrate these services inside a ‘flow’ or ‘use case’.
For example, let’s say that when a user registers with our application,
we send out a welcome email and set up some default preferences.
The two services,
respectively, are called from within a
RegisterUser flow. Flows
are powerful tools for domain modelling, but a little out of scope here.
Services live in
This comes down to personal preference, but I think services are most at
lib/ rather than
app/. The reason for this they behave
almost like libraries – they don’t depend on the rest of the application to
function. Hypothetically, a collection of services could be extracted and
plugged into another app without any problems, as long as you wanted the
Here’s an example of a simple service that sends a welcome email to a user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
So what we have is a class that has a single entry point in
as an instance and class method. We would usually go for
SendWelcomeEmail.call user: new_user when the service is called. It does
exactly one thing: sends the welcome email after checking if the user
has previously been welcomed. The service is completely independent of
the rest of the app, and easy to test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
It is easy to go overboard with services, falling into a pattern of extracting business logic into more and more services. Services are best in a complex application with a lot of business logic that requires testing and validation. It’s totally fine to have your business logic in a model, but the power of services and orchestrating them into flows really shines where you are modelling complex real world problems, such as invoicing, lead generation and resource management.
I think that service objects are an excellent way of modelling your domain(s): real-world processes can be broken down into steps, which are written as services and orchestrated in flows.
This allows the application to tell the developers what the behaviour is without having to deal with endless callbacks and tightly coupled models; letting the models act as a pure persistance layer. Controllers can call services directly to slim them down, or call flows if the action itself requires a chain of events to occur.
Ruby is an immensely expressive language with perhaps the best object-oriented heritage going, and by leveraging good OO practices we can make our Rails apps more testable, more adaptable and more maintainable.