Ruby/Rails

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.

I'm a Rubyist

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.

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.

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.

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.

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.

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.

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.

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.

This article has a good list of best practices for service objects.

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.

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.

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.

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.

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.