Service Objects
Service Objects are a popular software design pattern in the Ruby on Rails community. They are used to extract procedural logic away from models and controllers and into their own objects. Their popularity likely owes to the simplicity of the pattern and how easy it is to implement.
Fundamentally, they are objects with a single public method that is usually (but not always) named call
. That method implements some procedural business logic and returns a value representing the result of the operation.
I’ve compiled everything you need to know about when to use service objects and documented all the common variations of the pattern so that you can choose the right conventions for your project.
While this article focuses on service objects in the context of Ruby on Rails applications, most of the article applies equally to the pattern when implemented in other languages and frameworks.
History
Early Rails projects tended to accumulate complex logic in their controllers. This made those controllers challenging to test and often resulted in duplicated code whenever a piece of logic needed to be reused in a different part of the application.
Since most Rails applications are data-driven, a movement arose in the community towards “Skinny Controller, Fat Model.” This approach moved non-trivial domain logic into the model, rather than the controller, keeping the controllers relatively lean. This allow the business logic to be reused across controllers without duplication.
“Skinny Controller, Fat Model” worked well for CRUD applications and relatively simple projects, but more complex applications found issues with it. Models would grow in complexity, accumulating all the domain logic, resulting in difficult-to-test God objects.
The most common criticism of this approach was that no classes in your system should be “fat”; most object-oriented programmers will tell you that large classes should be decomposed and refactored into more cohesive, focused ones.
While some programmers reached for more complex domain-modelling techniques to decompose their objects, the service object pattern arose as a simple way to keep domain logic out of controllers and models, make it easier to test, and allow it to be reused across the application.
Structure
The community has yet to settle on a specific structure for service objects, but most approaches involve a class with a single public method. We’ll use account deletion as an example.
class AccountDeletionService
def delete(user)
return false if user.discarded?
user.discard!
AccountDeletionNotification.call(user)
true
end
end
This is a trivial example (and we’ll discuss the issue with that later), but it demonstrates the basic idea of the service object pattern. It’s an object with a single public method that performs some piece of procedural business logic. It may or may not return a value. If it does, it often represents the result of the operation.
By extracting the logic from the model or controller, you can reuse it more easily and test it independently of the database or HTTP layer.
Within this basic template, there’s a ton of variation. For example, some people like to pass the arguments to the constructor rather than the public instance method.
class AccountDeletionService
def initialize(user)
@user = user
end
def delete
return false if @user.discarded?
@user.discard!
AccountDeletionNotification.call(@user)
true
end
end
Often people will name the public method call
and delegate the class method of the same name, allowing you to call the service like AccountDeletionService.(some_user)
.
class AccountDeletionService
def self.call(user)
new.call(user)
end
def call(user)
return false if user.discarded?
user.discard!
AccountDeletionNotification.call(user)
true
end
end
This then exposes a seam to add some dependency injection, if you like that pattern.
class AccountDeletionService
def self.call(user)
new.call(user)
end
def initialize(notification_service: AccountDeletionNotification)
@notification_service = notification_service
end
def call(user)
return false if user.discarded?
user.discard!
@notification_service.call(user)
true
end
end
This allows mocks to be injected, isolating the tests from the service’s collaborators. The downside of this approach is that tests no longer serve to document the public API of the class. If your project treats tests as documentation for consumers of the object, this test’s subject should be described_class.call(user)
, but we’re forced to use the “private” API in our tests to inject the mock. This isn’t necessarily a deal-breaker, but it’s worth measuring this against how you use tests on your project.
RSpec.describe AccountDeletionService do
subject {
described_class.new(notification_service: notification_service).call(user)
}
let(:user) { create :user }
let(:notification_service) { object_double AccountDeletionNotification, call: nil }
it "soft-deletes the user" do
expect { subject }
.to change { user.discarded? }
.from(false).to(true)
end
it "notifies the user that their account was deleted" do
subject
expect(notification_job).to have_received(:call).with(user)
end
end
It’s common to use a base class for service objects so that they can all use the same API. Note that the base class is called ApplicationService
, mirroring the Rails base class conventions (ApplicationController
, ApplicationJob
and ApplicationModel
).
class ApplicationService
class << self
def call(*args, **kwargs, &block)
new.call(*args, **kwargs, &block)
end
end
end
class AccountDeletionService < ApplicationService
def initialize(notification_service: AccountDeletionNotification)
@notification_service = notification_service
end
def call(user)
return false if user.discarded?
user.discard!
@notification_service.call(user)
true
end
end
Delegating the class method isn’t necessary. Some testing and design strategies (like London-style or Mockist TDD) encourage explicit object composition, urging developers to group compositional code. This allows developers to create different behaviour by passing in different collaborators, leveraging runtime polymorphism.
To achieve this design, we take the code above, drop the base class and remove the default value from the constructor’s argument.
class AccountDeletionService
def initialize(notification_service:)
@notification_service = notification_service
end
def call(user)
return false if user.discarded?
user.discard!
@notification_service.call(user)
true
end
end
What changes now is how the object is consumed. When a user deletes their account, you’d initialize the object with an instance of AccountDeletionNotification
.
AccountDeletionService.new(
notification_service: AccountDeletionNotification.new
).call(user)
If one day your application comes under attack and bots create thousands of fake users, you could reuse this service. You could write a Rake task that constructs the AccountDeletionService
with a null object instead of the usual notification object so that this operation doesn’t send out any emails. Alternatively, if you wanted to record information about all the deleted accounts, you could create a new notification service to handle that.
Keep in mind that the code examples I’ve used are trivial and don’t necessarily warrant refactoring on their own. It’s reasonable to leave logic this simple in a controller if you don’t need to reuse it or move it to the user model if you do. This pattern is for when business logic grows unwieldy and warrants refactoring and decomposition. The examples are kept simple to better showcase the pattern without getting bogged down in the incidentals.
Naming
While most service objects look broadly similar, various popular naming conventions exist.
You can suffix the class name with Service
, leading to names like OrderDispatchService
, ReservationChangeService
and UserUpdateService
. These names communicate that the class represents a service that performs the given action.
All three of those last examples work just fine without the suffix. OrderDispatch
, ReservationChange
and UserUpdate
are all noun phrases that indicate that instances of that object are the named action or operation.
Another popular technique is what I call “doer” naming. This convention has the classes named for what they do rather than what action they are. This gives you names like OrderDispatcher
, ReservationChanger
and UserUpdater
. Some consider this naming convention an anti-pattern.
The community’s lack of naming conventions isn’t limited to class names; the method names also vary. call
is the most popular as many people like the shorthand syntax, MyService.(args)
, but some choose to switch it out for an action-related word like perform
.
Those that use the “doer” naming sometimes change the method’s name on a class-by-class basis. An OrderDispatcher
class would likely have a public dispatch
method under this convention. This can make the class method delegation pattern trickier to implement.
Regardless of naming preferences, make sure your choice aligns with how other kinds of classes are named on your project.
Namespacing
When you have many services that operate on the same resources, it’s common to namespace them. Rather than having a UserDeletionService
, UserUpdateService
and UserRegistrationService
, you might nest them all under Users
, resulting in Users::DeletionService
, Users::UpdateService
, and Users::RegistrationService
. Note that the namespace is pluralized to avoid some minor inconveniences that would arrive from nesting them inside the User
ActiveRecord model itself.
Where do they go?
Rails applications don’t ship with a dedicated folder for service objects. This makes sense because the framework doesn’t have (and doesn’t need) specific support for the pattern. Many projects extend the conventions set out by Rails and create an app/services
folder.
Projects with many different kinds of domain objects that fall outside the confines of models and controllers but aren’t all strictly speaking service objects may choose to put all those objects in an app/domain
folder. This creates a single folder for all the non-Rails classes that hold the application’s business logic.
You can also put your service objects in app/models
alongside everything else. This strategy represents a reframing of what “models” means. Projects that do this choose to make app/models
a place where the application’s domain-model lives without segregating classes between database models, service classes, value objects, POROs, and any other types of objects you might choose to create.
The final place you sometimes find service objects is in lib/services
. Generally speaking, the lib
folder is meant for support code that isn’t specific to your application’s domain, so this is not an appropriate place for service objects. I only mention it because you’ll find some articles recommending it.
Benefits
The previous sections touch on some of the benefits, but I’ll enumerate them here.
Testability
When logic in models or controllers gets too complex, testing it independently of the other logic in that layer can become difficult. Rather than write tests that exercise your branching business logic and HTTP concerns (query parameters, redirects, status codes, etc.), service objects allow you to exhaustively test your business logic’s branches separately. This allows you to simplify your controller (or model) tests and keep them focused on testing behaviour relevant to that layer of the system.
This argument really only applies when comparing the services objects to putting your logic in the controller or model layer.
Reusability
They offer better reusability than putting your logic in the controller layer because they can be invoked from any part of the system. However, this doesn’t make them significantly more reusable than putting your logic in your models, since those can be easily invoked wherever you want as well.
Composability
Because service objects can be constructed with different collaborators, they offer the opportunity to build new logic in your system through composition. Their relatively consistent APIs make arranging them in new ways easier.
For example, any two services that both take in a user and return a boolean value can be substituted for each other, though it may not produce a desirable result.
Simplicity
The big benefit service objects have over most other patterns is that they are easy to implement. You need to spend relatively little effort on designing the public API of a service object. The conventions on your project dictate whether you inject collaborators or hardcode them. They only have one method, and it’s probably named call
. The arguments to call
are whatever data the business logic needs. Besides naming the arguments, your work is basically done for you.
More advanced domain-modelling techniques require you to design the APIs of your objects, look for patterns, and think about shared interfaces. Service objects require none of that. They allow relatively inexperienced object-oriented programmers to build up fairly complex logic without getting bogged down in design.
This simplicity also has benefits when developers encounter service objects in new codebases. It’s easy to tell what a service object does and how to use it, even if you’ve never heard of a service object before.
Criticism
While service objects are heavily used in the Rails community, they are also criticized by some community members.
How I code without service objects - Code with Jason
What service objects are Service objects and Interactors are fashionable among Ra...
Jason Swett has discussed service objects on multiple occasions. In his latest article, he points out that the term “service objects” is unfortunately overloaded, but clarifies that he means roughly the pattern I’ve described above.
He believes that programmers conflate the pattern with a “paradigm” and use it as the primary building block for creating the business logic of their application and contends that programmer would be better served by “regular old OOP.” His main critique of the pattern itself is that it lends itself to very procedural code and that a more declarative style is superior.
Why Service Objects are an Anti-Pattern
Article after article has been published in recent years about the benefits of ad...
Jared White’s position is similar to Swett’s original take, but with ActiveSupport concerns in the mix. He lays out how concerns can complement domain objects to improve on a design where the bulk of the logic was in one big service object. This showcases concerns as another tool for improving reusability in designs.
He also takes issue with the fact that service objects can grow very large and don’t intrinsically improve the design of systems. The problem of objects accreting too much logic can happen with any pattern if developers don’t spend enough time refactoring, but it highlights the perils of naively moving all your complexity into large service objects. Besides being more reusable, a massive service object is about as unwieldy as a massive controller action.
Enough With the Service Objects Already
A critique of the emerging advice to encapsulate business domain actions in "Serv...
One of the more interesting critiques comes from Avdi Grimm. He pulls a heuristic from Sandi Metz’s POODR to demonstrate how to reevaluate the design, identify that the service object is just what many languages call a procedure, and turn it into a module method.
He argues that a bad abstraction is worse than no abstraction. An unfocused object doesn’t offer the programmer guidance on what logic does or doesn’t belong in it, so it is liable to accrete more and more functionality. He’s also critical of the building up functionality of large trees of service objects as it can create implicit coupling that’s difficult to reason about or disentangle.
He compares service objects to the concept of “services” drawn from Eric Evan’s book, Domain Driven Design. His conclusion is that we should implement procedural logic as procedures rather than service objects. Once we’re more confident in the feature’s shape and what concepts are at play, procedures are easier to refactor into domain-driven designs.
My Thoughts
Service objects are a reasonable choice for relatively small applications that have outgrown CRUD and need to reuse and compose their business logic. They do not scale well to more complex applications, where they can result in deep trees of nested logic that are difficult to disentangle and challenging to test.
Object-oriented programmers have a rich set of domain-modelling techniques at their disposal. Service objects are essentially functions (or procedures). (Consider that their only public method is usually named call
.) Rather than shoehorning all our logic into procedural function lookalikes, we can use them sparingly to support other patterns like value objects, aggregates, repositories, factories, and other POROs.
My Stylistic Preferences
I showed above that there are many different structural variations when it comes to service objects. Here are my preferences.
Just like with other domain objects, I try to avoid hardcoded dependencies. I instead pass the dependencies to my service objects’ constructors. This allows them to be constructed with different dependencies in different situations.
The inputs (the data or objects the service is actually operating on) are passed to the call
. Passing them to call
rather than the constructor means you can’t memoize intermediate values on the instance, but you can reuse the instance multiple times against different inputs, which can be valuable for optimization purposes.
I don’t implement a class-level call
method because I prefer the composition of the objects to be explicit. If I frequently need the same composition, I will extract that into a class-level method to avoid duplication.
I don’t namespace services based on the data they operate on unless I have a very large number of them.
Further Reading
I’ve assembled every article I could find here on service objects. I’ve noted the subtle variations that distinguish each of these resources from the others.
Service Objects for Rails
How we use service objects in our Ruby on Rails app to implement business logic.
This article adds some extra infrastructure to the base service class to make the objects more flexible. The call
method of services in this article returns a response object. (This pattern is sometimes called “result object” or “result monad”.) Besides call
, the services also support call!
which propagates errors rather than wrapping them in a failure response. This would pair nicely with dry-monad’s Result monad.
Refactoring Your Rails App With Service Objects
Rails apps tend to start simple, with clean models and controllers. Then you star...
This article is notable because the service objects accept all their arguments to their constructor and there’s also a demonstration of how to use the (now deprecated) BusinessProcess gem.
Using Service Objects in Ruby on Rails | AppSignal Blog
Find out what service objects are and why you should use them.
This article also features services that take their arguments to their constructors. It has a good walkthrough of how you can decompose logic into services, organize them in your codebase, and then compose the services to get different functionality in different situations.
For some reason, it uses OpenStruct for its response objects, which I generally think is a bad idea. A regular [Struct], a Data class, or a Result monad would all be strictly better in performance and semantics. The services also accept hashes rather than keyword arguments, which I wouldn’t recommend.
Rails Service Objects: A Comprehensive Guide | Codete Blog
Rails Service Objects overview. Learn more about designing clean code with Ruby o...
I have two gripes about this blog post: First, its code snippets are highlighted using a mix of monospace and variable-width fonts. It doesn’t help their legibility. Second, he uses the word “equipollent”. Come on, dude. You didn’t need to make me check my dictionary.
The article itself is a fairly straightforward tutorial on creating service objects that take their arguments to their constructor. Be warned that the argument forwarding in the base class doesn’t support keyword arguments.
Using Rails Service Objects to Keep Code Clean
What are Service Objects and how you can use them to make your app cleaner and ke...
This article has a good list of best practices for service objects.
Where did concept of service object come from? : rails
This Reddit post has some good discussion about service objects. One thing to watch out for is that there’s a bit of misinformation mixed in. For example, service objects are not always an implementation of the command pattern, though they can be.
Rails Service Objects: A Comprehensive Guide | Toptal®
Sometimes, your business logic can't fit in the either a model or a controller. T...
This article has good SEO and so gets referenced a lot. It’s got largely pretty good info, but I take issue with rule 3: “Don’t Create Generic Objects to Perform Multiple Actions”. The great thing about object-oriented programming is that we can create intuitive objects that group together behaviour and state. If you have two pieces of related behaviour that operate on the same data, you should absolutely consider making an object that supports both those behaviours.
Skinny Controllers, Skinny Models
Does a simple controller mean a complex model?
This fairly old blog post urges readers to give up on “Fat Model, Skinny Controller” and refactor our logic into domain objects. You might call it prescient, but object-oriented programming was extremely mature when it was written. I suspect that the Rails community’s aversion to anything associated with Java is partly to blame for why this kind of thinking isn’t more dominant.
Skinny Models, Skinny Controllers, Fat Services
It is likely you have heard ‘Skinny Controllers, Fat Models’ as a cute little bes...
This article also isn’t about service objects. It uses the term “service” to describe domain objects. This is exactly what many critics of service objects are advocating for, though the idea that we make them “fat” is exactly what Jared White argues against.
All Rails service objects as one Ruby class
All Rails service objects as one Ruby class I review many Rails applications ever...
This isn’t as much a tutorial as a thought experiment where the author has implemented all the logic that would have gone into service objects as one giant service object with many methods. This doesn’t seem like a good idea, but taking ideas to extremes is a great way to learn more about why we do things the way we do.