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.
by Jared Norman
updated February 26, 2024

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 Rails developers today. I can’t speak for everyone’s motives but the impression I get is that service objects are seen as a way to avoid the “fat models” you’re left with if you follow the “skinny controllers, fat models” guideline. If your models are […]

Code with Jason

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 adding service objects to Rails applications. I’m here to tell you they’re wrong. There is a better way.

Fullstack Ruby

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 "Service Objects".

avdi.codes - Avdi Grimm, Code Cleric

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.

Medium

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 start adding features. Before you know it, your models and controllers are big, unwieldy, and hard to understand. Refactoring into service objects is a great way to split these big pieces up, so they're easier to underst...

Honeybadger Developer Blog

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.

AppSignal Blog

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 on Rails Service Objects and other ins & outs of making use of them.

Codete Blog

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 keep it maintainable

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

Where did concept of service object come from? : rails
r/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. That's where service objects come in, where you can separate every business action into its own service object.

Toptal Engineering Blog

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?

thoughtbot

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 best-practice-saying to follow for your MVC application…

Medium

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 every month, every year. One visible change is that service objects became mainstream in the Rails community. This makes me happy, as I believe they do introduce some more order in typical Rails apps. Service objects were the main topic of my “Fearless Refactoring: Rails controllers” book, along with adapters, repositories and form objects. Today I’d like to present one technique for grouping service objects.

Arkency Blog

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.