Better Rails Service Objects With dry-rb

Aref Aslani
Sep 13 · 4 min read
Image for post
Image for post

The idea of using service objects came into my mind when I first read about the DCI architecture in Clean Ruby.

The paradigm separates the domain model (data) from use cases (context) and Roles that objects play (interaction). DCI is complementary to model–view–controller (MVC). MVC as a pattern language is still used to separate the data and its processing from presentation.

As the book and the quote from Wikipedia suggest, using this architecture in our code makes our models only contain data (we will even migrate data validations to service objects) and controller actions just call one of these interactions and render the result.

There are many cool libraries that I’ve used as the interaction layer like ActiveInteraction, Interactor, or Trailblazer Operations. But each of them misses something that I like which is present in another library. So I decided to pick and combine every piece that I like from these solutions and create mine.

To better understand the upcoming concepts, let’s consider a very simple blog app where it has a Post model with title and body.

Basic API

Let’s go back to our simple example. Suppose that we need a service to create a blog post. We will define a class named Posts::Create and will add a call class method to it that receives a params hash.

Adding validations

Ok. I agree that in comparison to the previous code snippet, I added a lot of things to this one. But believe me, they’re all super simple. Let’s check what we’ve added. For now, let’s skip the line number 3 and start with line number 5. Here we’ve added the validation schema for our service object. For this purpose, I’ve used dry-schema gem. It’s easy to understand and way more flexible than ActiveSupport validations. Check out all the available schema validation rules at dry-schema documentation. As you see, here we’ve added two rules:

  1. Title is required and must be a string
  2. Body is optional, but if it’s present, it should be also a string.

Then in the .call class method, we instantiate an instance of the service and call it’s execute method and will pass all the parameters to it. In the execute, method, we’ll call all the steps needed to fulfill all the requirements of this operation. As you see, every step is simply a method and we call them one after another.

We’ll use Success and Failure functions to specify if the result of each step was successful or a failure. These functions are provided in dry-monads gem which you can read more about in its documentation website. To have access to these functions in our service objects, we need to include the result monad using

include Dry::Monads[:result]

specified in line 3 of the previous code snippet. They’ll wrap the result of each step in a Success or a Failure object and then we can unwrap them using the yield keyword that does two things in this case:

  1. Unwraps the value of a Success or a Failure object.
  2. Halts the execution of the method if there a failure in any of the steps.

To be able to use this monad, we need to add the do monad to our include statement:

include Dry::Monads[:result, :do]

Now if for example, the validation step returns a failure object, then the execution of the operation halts, and a Failure object containing the errors will be returned.

Handling output

Beautiful, right? Let’s add it to our service object. We only need to use ResultMatcher from the dry-matchers gem for that. In the .call method, if a block is given, then we need to wrap the response in the ResultMatcher object:

Making it reusable

After separating the base logic from the main class, your operation classes will be as simple as this:

You only need to inlcude the ApplicationServiceand define an execute method in your service object and if you add the ValidationSchema constant, then it’ll automatically pick and call it.

We also can go further and add dry-validation to make our validations more powerful. But I need to leave the computer and make a cup of tea for myself, so I’ll give it over to you.

Happy hacking :)

The Startup

Medium's largest active publication, followed by +730K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store