DEV Community

Cover image for Effective Service Objects in Ruby
Matteo Joliveau for MIKAMAI

Posted on • Updated on

Effective Service Objects in Ruby

As a Java developer that recently transitioned to a Ruby on Rails company, I felt kinda lost when I discovered that the use of models directly inside of controllers was a common practice.

I have always followed the good practices of Domain Driven Design and encapsulated my business logic inside special classes called service objects, so in Java (with Spring) a controller would look like this:

@Controller
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public ResponseEntity<Iterable<User>> getAllUsers() {
        return ResponseEntity.ok(userService.getUsers());
    }
}
Enter fullscreen mode Exit fullscreen mode

Verbosity aside, this is nice and clean with good separation of concerns. The actual business logic that retrieves the user list is delegated to the UserService implementation and can be swapped out at any time.

However, in Rails we would write this controller as such:

class Api::UserController < ApplicationController
    def index
        @users = User.all
        render json: @users
    end
end
Enter fullscreen mode Exit fullscreen mode

Yes, this is indeed shorter and even cleaner than the Java example, but it has a major flaw. User is an ActiveRecord model, and by doing this we are tightly coupling our controller to our persistence layer, breaking one of the key aspects of DDD. Moreover, if we wanted to add authorization checks to our requests, maybe only returning a subgroup of users based on the current user's role, we would have to refactor our controller and putting it in charge of something that is not part of the presentation logic. By using a service object, we can add more logic to it while being transparent to the rest of the world.

Let's build a service object

In Java this is simple. It's a singleton class that is injected into other classes by our IoC container (Spring DI in our example).
In Ruby, and Rails especially, this is not quite the same, since we can't really inject anything in our controller constructor. What we can do, however, is taking inspiration by another programming language: Elixir.
In Elixir, a functional language, there are no classes nor objects, only functions and structs. Functions are grouped into modules and have no side effects, a great feature to ensure immutability and stability in our code.
Since Ruby too has modules, we can use them to implement our service object as stateless collections of methods.

Our UserService can look something like this:

module UserService
    class << self
        def all_users
            User.all
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

And then will be used like this:

class Api::UserController < ApplicationController
    def index
        @users = UserService.all_users
        render json: @users
    end
end
Enter fullscreen mode Exit fullscreen mode

This doesn't sound like a smart move, does it? We just moved the User.all call in another class. And that's true, but now, as our application grow we can add more logic to it without breaking other code or refactoring, as long as we keep our API stable.
One small change I'll make before proceding. Since we may want to inject some data into our service on every call, we'll define our methods with a first parameter named ctx, which will contain the current execution context. Stuff like the current user and such will be contained there.

module UserService
    class << self
        def all_users _ctx # we'll ignore it for now
            User.all
        end
    end
end
Enter fullscreen mode Exit fullscreen mode
class Api::UserController < ApplicationController
    def index
        @users = UserService.all_users { current_user: current_user }
        render json: @users
    end
end
Enter fullscreen mode Exit fullscreen mode

Applying business logic

Now let's build a more complex case, and let's use a user story to describe it first. Let's imagine we're building a ToDo app (Wow, how revolutionary!).
The story would be:

As a normal user I want to be able to see all my todos for the next month.

The RESTful HTTP call will be something like:
GET /api/todos?from=${today}&to=${today + 1 month}

Our controller will be:

class Api::TodoController < ApplicationController
    def index
        @ctx = { current_user: current_user }
        @todos = TodoService.all_todos_by_interval @ctx, permitted_params
        render json: @todos
    end

    private

    def permitted_params
        params.require(:todo).permit(:from, :to)
    end
end
Enter fullscreen mode Exit fullscreen mode

And our service:

module TodoService
    class << self
        def all_todos_by_interval ctx, params
            Todos.where(user: ctx[:current_user]).by_interval params
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

As you can see we are still delegating the heavy database lifting to the model (throught the scope by_interval) but the service is actually in control of filtering only for the current user. Our controller stays skinny, our model is used only for persistence access, and our business logic doesn't leak in every corner of our source code. Yay!

Service Composition

Another very useful OOP pattern we can use to enhance our business layer is the composite pattern. With it, we can segregate common logic into dedicated, opaque services and call them from other services. For example we might want to send a notification to the user when a todo is updated (for instance because it expired). We can put the notification logic into another service and call it from the previous one.

module TodoService
    class << self
        def update_todo ctx, params
            updated_todo = Todos.find ctx[:todo_id]
            updated_todo.update! params # raise exception if unable to update
            notify_expiration ctx[:current_user], updated_todo if todo.expired?
        end

        private

        def notify_expiration user, todo # put in a private method for convenience
            NotificationService.notify_of_expiration { current_user: user }, todo
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

Commands for repetitive tasks

As the Gang of Four gave us a huge amount of great OOP patterns, I'm going to borrow one last concepts from them and greatly increase our code segregation. You see, our services could act as coordinators instead of executors, delegating the actual work to other classes and only caring about calling the right ones. Those smaller, "worker-style" classes can be implemented as commands. This has the biggest advantage of enhancing composition by using smaller execution units (single commands instead of complex services) and separating concerns even more. Now services act as action coordinators, orchestrating how logic is executed, while the actual execution is run inside simple, testable and reusable components.

Side Note: I'm going to use the gem simple_command to implement the command pattern, but you are free to use anything you want

Let's refactor the update logic to use the command pattern:

class UpdateTodo
    prepend SimpleCommand

    def initialize todo_id, params
        @todo_id = todo_id
        @params = params
    end

    def call
        todo = Todos.find @todo_id

        # gather errors instead of throwing exception
        errors.add_multiple_errors todo.errors unless todo.update @params
        todo
    end
end

module TodoService
    class << self
        def update_todo ctx, params
            cmd = UpdateTodo.call ctx[:todo_id], params

            if cmd.success?
                todo = cmd.result
                notify_expiration ctx[:current_user], todo if todo.expired?
            end

            # let's return the command result so that the controller can
            # access the errors if any
            cmd
        end

        private

        def notify_expiration user, todo # put in a private method for convenience
            NotificationService.notify_of_expiration { current_user: user }, todo if todo.expired?
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

Beautiful. Now every class has one job (Controllers receive requests and return responses, Commands execute small tasks and Services wire everything together), our business logic is easily testable without needing any supporting infrastructure (just mock everything. Mocks are nice.) and we have smaller and more reusable methods. We just have a slightly bigger codebase, but it's still nothing compared to a Java project and it's worth the effort on the long run.
Also, our services are no longer coupled to any Rails (or other frameworks) specific class. If for instance we wanted to change the persistence library, or migrate one business domain to an external microservice, we just have to refactor the related commands without having to touch our services.

Are you using service objects in your Ruby projects? How did you implement the pattern and what challenges did you solved that my approach does not?

Top comments (8)

Collapse
 
aboub_g profile image
Abou Bakr G. • Edited

I used Service Objects in my last project with the same gem simple_command, that was a beautiful experience. And it helps me find bugs more quickly. In your approach, you split the code in a better way. I think it gives more control on the code base. As the code base grows, you will have less pain to maintain it.
Thanks for sharing.

Collapse
 
matteojoliveau profile image
Matteo Joliveau • Edited

I tend to use commands only for small tasks like creating/updating/filtering models, and not for full-fledged services because it leads to an incredible proliferation of classes. If you have 4 different models and you implement the basic CRUD commands for each of them, you end up with 16 different classes. Aggregating them in service modules and using those in your business code allows for more clarity and consistency (you know that all the user-related functionality lives inside UserService etc) while leveraging your 10s of small commands under the hood.

Collapse
 
maestromac profile image
Mac Siri

Nice post! Since UpdateTodo is very much related to TodoService, you could keep it under the TodoService namespace, like TodoService::UpdateTodo.

Collapse
 
matteojoliveau profile image
Matteo Joliveau

This is a really valid point, thanks for the idea!
It also helps to keep files more organized in the directory structure

Collapse
 
ben profile image
Ben Halpern

How does this jive with where your head is at with all this @maestromac ?

Collapse
 
maestromac profile image
Mac Siri

Thanks for tagging me! SimpleCommand gem seems really useful.

Collapse
 
knovak72 profile image
knovak72

I use to write my own service objects until I discovered the LightService gem. It provides a lot more functionality than the roll-your-own version.

Collapse
 
matteojoliveau profile image
Matteo Joliveau

Didn't know it, at first glance it seems like a more featureful command library than simple_command.
Interesting, but we're not talking about the same kind of service objects here. LightService and simple_command give you a single operation (or command, or action) implemented in each class, while a service object in my context (and the canonical design pattern) is more of a swiss army knife aggregating all the needed functionalities related to a domain element.